tksbrokerapi.TKSBrokerAPI

TKSBrokerAPI is the trading platform for automation and simplifying the implementation of trading scenarios, as well as working with Tinkoff Invest API server via the REST protocol. The TKSBrokerAPI platform may be used in two ways: from the console, it has a rich keys and commands, or you can use it as Python module with python import.

TKSBrokerAPI allows you to automate routine trading operations and implement your trading scenarios, or just receive the necessary information from the broker. It is easy enough to integrate into various CI/CD automation systems.

   1# -*- coding: utf-8 -*-
   2# Author: Timur Gilmullin
   3
   4"""
   5**TKSBrokerAPI** is the trading platform for automation and simplifying the implementation of trading scenarios,
   6as well as working with Tinkoff Invest API server via the REST protocol. The TKSBrokerAPI platform may be used in two ways:
   7from the console, it has a rich keys and commands, or you can use it as Python module with `python import`.
   8
   9TKSBrokerAPI allows you to automate routine trading operations and implement your trading scenarios, or just receive
  10the necessary information from the broker. It is easy enough to integrate into various CI/CD automation systems.
  11
  12- **Open account for trading:** http://tinkoff.ru/sl/AaX1Et1omnH
  13- **TKSBrokerAPI module documentation:** https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSBrokerAPI.html
  14- **See CLI examples:** https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md#Usage-examples
  15- **Used constants are in the TKSEnums module:** https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSEnums.html
  16- **About Tinkoff Invest API:** https://tinkoff.github.io/investAPI/
  17- **Tinkoff Invest API documentation:** https://tinkoff.github.io/investAPI/swagger-ui/
  18"""
  19
  20# Copyright (c) 2022 Gilmillin Timur Mansurovich
  21#
  22# Licensed under the Apache License, Version 2.0 (the "License");
  23# you may not use this file except in compliance with the License.
  24# You may obtain a copy of the License at
  25#
  26#     http://www.apache.org/licenses/LICENSE-2.0
  27#
  28# Unless required by applicable law or agreed to in writing, software
  29# distributed under the License is distributed on an "AS IS" BASIS,
  30# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  31# See the License for the specific language governing permissions and
  32# limitations under the License.
  33
  34
  35import sys
  36import os
  37from argparse import ArgumentParser
  38from importlib.metadata import version
  39
  40from dateutil.tz import tzlocal
  41from time import sleep
  42
  43import re
  44import json
  45import requests
  46import traceback as tb
  47from typing import Union
  48
  49from multiprocessing import cpu_count, Lock
  50from multiprocessing.pool import ThreadPool
  51import pandas as pd
  52
  53from mako.template import Template  # Mako Templates for Python (https://www.makotemplates.org/). Mako is a template library provides simple syntax and maximum performance.
  54from Templates import *  # Some html-templates used by reporting methods in TKSBrokerAPI module
  55from TKSEnums import *  # A lot of constants from enums sections: https://tinkoff.github.io/investAPI/swagger-ui/
  56from TradeRoutines import *  # This library contains some methods used by trade scenarios implemented with TKSBrokerAPI module
  57
  58from pricegenerator.PriceGenerator import PriceGenerator, uLogger  # This module has a lot of instruments to work with candles data (https://github.com/Tim55667757/PriceGenerator)
  59from pricegenerator.UniLogger import DisableLogger as PGDisLog  # Method for disable log from PriceGenerator
  60
  61import UniLogger as uLog  # Logger for TKSBrokerAPI
  62
  63
  64# --- Common technical parameters:
  65
  66PGDisLog(uLogger.handlers[0])  # Disable 3-rd party logging from PriceGenerator
  67uLogger = uLog.UniLogger  # init logger for TKSBrokerAPI
  68uLogger.level = 10  # debug level by default for TKSBrokerAPI module
  69uLogger.handlers[0].level = 20  # info level by default for STDOUT of TKSBrokerAPI module
  70
  71__version__ = "1.6"  # The "major.minor" version setup here, but build number define at the build-server only
  72
  73CPU_COUNT = cpu_count()  # host's real CPU count
  74CPU_USAGES = CPU_COUNT - 1 if CPU_COUNT > 1 else 1  # how many CPUs will be used for parallel calculations
  75
  76
  77class TinkoffBrokerServer:
  78    """
  79    This class implements methods to work with Tinkoff broker server.
  80
  81    Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/
  82
  83    About `token`: https://tinkoff.github.io/investAPI/token/
  84    """
  85    def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None:
  86        """
  87        Main class init.
  88
  89        :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`.
  90        :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports.
  91                          Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.
  92        :param useCache: use default cache file with raw data to use instead of `iList`.
  93                         True by default. Cache is auto-update if new day has come.
  94                         If you don't want to use cache and always updates raw data then set `useCache=False`.
  95        :param defaultCache: path to default cache file. `dump.json` by default.
  96        """
  97        if token is None or not token:
  98            try:
  99                self.token = r"{}".format(os.environ["TKS_API_TOKEN"])
 100                uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/")
 101
 102            except KeyError:
 103                uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/")
 104                raise Exception("Token required")
 105
 106        else:
 107            self.token = token  # highly priority than environment variable 'TKS_API_TOKEN'
 108            uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`")
 109
 110        if accountId is None or not accountId:
 111            try:
 112                self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"])
 113                uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId))
 114
 115            except KeyError:
 116                uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).")
 117
 118        else:
 119            self.accountId = accountId  # highly priority than environment variable 'TKS_ACCOUNT_ID'
 120            uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId))
 121
 122        self.version = __version__  # duplicate here used TKSBrokerAPI main version
 123        """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.
 124
 125        Latest version: https://pypi.org/project/tksbrokerapi/
 126        """
 127
 128        self.__lock = Lock()  # initialize multiprocessing mutex lock
 129
 130        self.aliases = TKS_TICKER_ALIASES
 131        """Some aliases instead official tickers.
 132
 133        See also: `TKSEnums.TKS_TICKER_ALIASES`
 134        """
 135
 136        self.aliasesKeys = self.aliases.keys()  # re-calc only first time at class init
 137
 138        self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED  # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there
 139
 140        self._ticker = ""
 141        """String with ticker, e.g. `GOOGL`. Tickers may be upper case only.
 142
 143        Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc.
 144        More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
 145
 146        See also: `SearchByTicker()`, `SearchInstruments()`.
 147        """
 148
 149        self._figi = ""
 150        """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only.
 151
 152        See also: `SearchByFIGI()`, `SearchInstruments()`.
 153        """
 154
 155        self.depth = 1
 156        """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI.
 157
 158        See also: `GetCurrentPrices()`.
 159        """
 160
 161        self.server = r"https://invest-public-api.tinkoff.ru/rest"
 162        """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest
 163
 164        See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`.
 165        """
 166
 167        uLogger.debug("Broker API server: {}".format(self.server))
 168
 169        self.timeout = 15
 170        """Server operations timeout in seconds. Default: `15`.
 171
 172        See also: `SendAPIRequest()`.
 173        """
 174
 175        self.headers = {
 176            "Content-Type": "application/json",
 177            "accept": "application/json",
 178            "Authorization": "Bearer {}".format(self.token),
 179            "x-app-name": "Tim55667757.TKSBrokerAPI",
 180        }
 181        """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`.
 182
 183        See also: `SendAPIRequest()`.
 184        """
 185
 186        self.body = None
 187        """Request body which send to broker server. Default: `None`.
 188
 189        See also: `SendAPIRequest()`.
 190        """
 191
 192        self.moreDebug = False
 193        """Enables more debug information in this class, such as net request and response headers in all methods. `False` by default."""
 194
 195        self.useHTMLReports = False
 196        """
 197        If `True` then TKSBrokerAPI generate also HTML reports from Markdown. `False` by default.
 198        
 199        See also: Mako Templates for Python (https://www.makotemplates.org/). Mako is a template library provides simple syntax and maximum performance.
 200        """
 201
 202        self.historyFile = None
 203        """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame.
 204
 205        See also: `History()`.
 206        """
 207
 208        self.htmlHistoryFile = "index.html"
 209        """Full path to the html file where rendered candles chart stored. Default: `index.html`.
 210
 211        See also: `ShowHistoryChart()`.
 212        """
 213
 214        self.instrumentsFile = "instruments.md"
 215        """Filename where full available to user instruments list will be saved. Default: `instruments.md`.
 216
 217        See also: `ShowInstrumentsInfo()`.
 218        """
 219
 220        self.searchResultsFile = "search-results.md"
 221        """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`.
 222
 223        See also: `SearchInstruments()`.
 224        """
 225
 226        self.pricesFile = "prices.md"
 227        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
 228
 229        See also: `GetListOfPrices()`.
 230        """
 231
 232        self.infoFile = "info.md"
 233        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
 234
 235        See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`.
 236        """
 237
 238        self.bondsXLSXFile = "ext-bonds.xlsx"
 239        """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 
 240        bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`.
 241
 242        See also: `ExtendBondsData()`.
 243        """
 244
 245        self.calendarFile = "calendar.md"
 246        """Filename where bonds payment calendar will be saved. Default: `calendar.md`.
 247        
 248        Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`.
 249
 250        See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`.
 251        """
 252
 253        self.overviewFile = "overview.md"
 254        """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`.
 255
 256        See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`.
 257        """
 258
 259        self.overviewDigestFile = "overview-digest.md"
 260        """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`.
 261
 262        See also: `Overview()` with parameter `details="digest"`.
 263        """
 264
 265        self.overviewPositionsFile = "overview-positions.md"
 266        """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`.
 267
 268        See also: `Overview()` with parameter `details="positions"`.
 269        """
 270
 271        self.overviewOrdersFile = "overview-orders.md"
 272        """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`.
 273
 274        See also: `Overview()` with parameter `details="orders"`.
 275        """
 276
 277        self.overviewAnalyticsFile = "overview-analytics.md"
 278        """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`.
 279
 280        See also: `Overview()` with parameter `details="analytics"`.
 281        """
 282
 283        self.overviewBondsCalendarFile = "overview-calendar.md"
 284        """Filename where only the bonds calendar section will be saved. Default: `overview-calendar.md`.
 285
 286        See also: `Overview()` with parameter `details="calendar"`.
 287        """
 288
 289        self.reportFile = "deals.md"
 290        """Filename where history of deals and trade statistics will be saved. Default: `deals.md`.
 291
 292        See also: `Deals()`.
 293        """
 294
 295        self.withdrawalLimitsFile = "limits.md"
 296        """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`.
 297
 298        See also: `OverviewLimits()` and `RequestLimits()`.
 299        """
 300
 301        self.userInfoFile = "user-info.md"
 302        """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`.
 303
 304        See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`.
 305        """
 306
 307        self.userAccountsFile = "accounts.md"
 308        """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`.
 309
 310        See also: `OverviewAccounts()`, `RequestAccounts()`.
 311        """
 312
 313        self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache
 314        """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`.
 315
 316        Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`.
 317
 318        See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`.
 319        """
 320
 321        self.iList = None  # init iList for raw instruments data
 322        """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`.
 323        
 324        See also: `Listing()`, `DumpInstruments()`.
 325        """
 326
 327        # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server:
 328        if useCache:
 329            if os.path.exists(self.iListDumpFile):
 330                dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc())  # dump modification date and time
 331                curTime = datetime.now(tzutc())
 332
 333                if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year):
 334                    uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
 335
 336                    self.DumpInstruments(forceUpdate=True)  # updating self.iList and dump file
 337
 338                else:
 339                    self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8"))  # load iList from dump
 340
 341                    uLogger.debug("Local cache with raw instruments data is used: [{}]. Last modified: [{}] UTC".format(
 342                        os.path.abspath(self.iListDumpFile),
 343                        dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT),
 344                    ))
 345
 346            else:
 347                uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...")
 348                self.DumpInstruments(forceUpdate=True)  # updating self.iList and creating default dump file
 349
 350        else:
 351            self.iList = self.Listing()  # request new raw instruments data from broker server
 352            self.DumpInstruments(forceUpdate=False)  # save raw instrument's data to default dump file `iListDumpFile`
 353
 354        self.priceModel = PriceGenerator()  # init PriceGenerator object to work with candles data
 355        """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.
 356
 357        See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator
 358        """
 359
 360    @property
 361    def ticker(self) -> str:
 362        """String with ticker, e.g. `GOOGL`. Tickers may be upper case only.
 363
 364        Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc.
 365        More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
 366
 367        See also: `SearchByTicker()`, `SearchInstruments()`.
 368        """
 369        return self._ticker
 370
 371    @ticker.setter
 372    def ticker(self, value):
 373        """Setter for string with ticker, e.g. `GOOGL`. Tickers may be upper case only.
 374
 375        Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc.
 376        More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
 377
 378        See also: `SearchByTicker()`, `SearchInstruments()`.
 379        """
 380        self._ticker = str(value).upper()  # Tickers may be upper case only
 381
 382    @property
 383    def figi(self) -> str:
 384        """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only.
 385
 386        See also: `SearchByFIGI()`, `SearchInstruments()`.
 387        """
 388        return self._figi
 389
 390    @figi.setter
 391    def figi(self, value):
 392        """Setter for string with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only.
 393
 394        See also: `SearchByFIGI()`, `SearchInstruments()`.
 395        """
 396        self._figi = str(value).upper()  # FIGI may be upper case only
 397
 398    def _ParseJSON(self, rawData="{}") -> dict:
 399        """
 400        Parse JSON from response string.
 401
 402        :param rawData: this is a string with JSON-formatted text.
 403        :return: JSON (dictionary), parsed from server response string. If an error occurred, then returns empty dict `{}`.
 404        """
 405        try:
 406            responseJSON = json.loads(rawData) if rawData else {}
 407
 408            if self.moreDebug:
 409                uLogger.debug("JSON formatted raw body data of response:\n{}".format(json.dumps(responseJSON, indent=4)))
 410
 411            return responseJSON
 412
 413        except Exception as e:
 414            uLogger.error("An empty dict will be return, because an error occurred in `_ParseJSON()` method with comment: {}".format(e))
 415            return {}
 416
 417    def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5) -> dict:
 418        """
 419        Send GET or POST request to broker server and receive JSON object.
 420
 421        self.header: must be defining with dictionary of headers.
 422        self.body: if define then used as request body. None by default.
 423        self.timeout: global request timeout, 15 seconds by default.
 424        :param url: url with REST request.
 425        :param reqType: send "GET" or "POST" request. "GET" by default.
 426        :param retry: how many times retry after first request if an 5xx server errors occurred.
 427        :param pause: sleep time in seconds between retries.
 428        :return: response JSON (dictionary) from broker.
 429        """
 430        if reqType.upper() not in ("GET", "POST"):
 431            uLogger.error("You can define request type: `GET` or `POST`!")
 432            raise Exception("Incorrect value")
 433
 434        if self.moreDebug:
 435            uLogger.debug("Request parameters:")
 436            uLogger.debug("    - REST API URL: {}".format(url))
 437            uLogger.debug("    - request type: {}".format(reqType))
 438            uLogger.debug("    - headers:\n{}".format(str(self.headers).replace(self.token, "*** request token ***")))
 439            uLogger.debug("    - body:\n{}".format(self.body))
 440
 441        # fast hack to avoid all operations with some tickers/FIGI
 442        responseJSON = {}
 443        oK = True
 444        for item in self.exclude:
 445            if item in url:
 446                if self.moreDebug:
 447                    uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude)))
 448
 449                oK = False
 450                break
 451
 452        if oK:
 453            with self.__lock:  # acquire the mutex lock
 454                counter = 0
 455                response = None
 456                errMsg = ""
 457
 458                while not response and counter <= retry:
 459                    if reqType == "GET":
 460                        response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout)
 461
 462                    if reqType == "POST":
 463                        response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout)
 464
 465                    if self.moreDebug:
 466                        uLogger.debug("Response:")
 467                        uLogger.debug("    - status code: {}".format(response.status_code))
 468                        uLogger.debug("    - reason: {}".format(response.reason))
 469                        uLogger.debug("    - body length: {}".format(len(response.text)))
 470                        uLogger.debug("    - headers:\n{}".format(response.headers))
 471
 472                    # Server returns some headers:
 473                    # - `x-ratelimit-limit` — shows the settings of the current user limit for this method.
 474                    # - `x-ratelimit-remaining` — the number of remaining requests of this type per minute.
 475                    # - `x-ratelimit-reset` — time in seconds before resetting the request counter.
 476                    # See: https://tinkoff.github.io/investAPI/grpc/#kreya
 477                    if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0":
 478                        rateLimitWait = int(response.headers["x-ratelimit-reset"])
 479                        uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait))
 480                        sleep(rateLimitWait)
 481
 482                    # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes
 483                    if 400 <= response.status_code < 500:
 484                        msg = "status code: [{}], response body: {}".format(response.status_code, response.text)
 485                        uLogger.debug("    - not oK, but do not retry for 4xx errors, {}".format(msg))
 486
 487                        if "code" in response.text and "message" in response.text:
 488                            msgDict = self._ParseJSON(rawData=response.text)
 489                            uLogger.warning("HTTP-status code [{}], server message: {}".format(response.status_code, msgDict["message"]))
 490
 491                        counter = retry + 1  # do not retry for 4xx errors
 492
 493                    if 500 <= response.status_code < 600:
 494                        errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text)
 495                        uLogger.debug("    - not oK, {}".format(errMsg))
 496
 497                        if "code" in response.text and "message" in response.text:
 498                            errMsgDict = self._ParseJSON(rawData=response.text)
 499                            uLogger.warning("HTTP-status code [{}], error message: {}".format(response.status_code, errMsgDict["message"]))
 500
 501                        counter += 1
 502
 503                        if counter <= retry:
 504                            uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause))
 505                            sleep(pause)
 506
 507                responseJSON = self._ParseJSON(rawData=response.text)
 508
 509                if errMsg:
 510                    uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/")
 511                    uLogger.error("    - not oK, {}".format(errMsg))
 512
 513        return responseJSON
 514
 515    def _IUpdater(self, iType: str) -> tuple:
 516        """
 517        Request instrument by type from server. See available API methods for instruments:
 518        Currencies: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Currencies
 519        Shares: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Shares
 520        Bonds: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Bonds
 521        Etfs: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Etfs
 522        Futures: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Futures
 523
 524        :param iType: type of the instrument, it must be one of supported types in TKS_INSTRUMENTS list.
 525        :return: tuple with iType name and list of available instruments of current type for defined user token.
 526        """
 527        result = []
 528
 529        if iType in TKS_INSTRUMENTS:
 530            uLogger.debug("Requesting available [{}] list. Wait, please...".format(iType))
 531
 532            # all instruments have the same body in API v2 requests:
 533            self.body = str({"instrumentStatus": "INSTRUMENT_STATUS_UNSPECIFIED"})  # Enum: [INSTRUMENT_STATUS_UNSPECIFIED, INSTRUMENT_STATUS_BASE, INSTRUMENT_STATUS_ALL]
 534            instrumentURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/{}".format(iType)
 535            result = self.SendAPIRequest(instrumentURL, reqType="POST")["instruments"]
 536
 537        return iType, result
 538
 539    def _IWrapper(self, kwargs):
 540        """
 541        Wrapper runs instrument's update method `_IUpdater()`.
 542        It's a workaround for using multiprocessing with kwargs. See: https://stackoverflow.com/a/36799206
 543        """
 544        return self._IUpdater(**kwargs)
 545
 546    def Listing(self) -> dict:
 547        """
 548        Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.
 549
 550        :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.
 551        """
 552        uLogger.debug("Requesting all available instruments for current account. Wait, please...")
 553        uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES))
 554
 555        # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService
 556        # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list.
 557        iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS]
 558
 559        poolUpdater = ThreadPool(processes=CPU_USAGES)  # create pool for update instruments in parallel mode
 560        listing = poolUpdater.map(self._IWrapper, iParams)  # execute update operations
 561        poolUpdater.close()  # close the thread pool
 562        poolUpdater.join()  # wait a moment until all data returns from threads
 563
 564        # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures.
 565        # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method
 566        iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing}
 567
 568        # calculate minimum price increment (step) for all instruments and set up instrument's type:
 569        for iType in iList.keys():
 570            for ticker in iList[iType]:
 571                iList[iType][ticker]["type"] = iType
 572
 573                if "minPriceIncrement" in iList[iType][ticker].keys():
 574                    iList[iType][ticker]["step"] = NanoToFloat(
 575                        iList[iType][ticker]["minPriceIncrement"]["units"],
 576                        iList[iType][ticker]["minPriceIncrement"]["nano"],
 577                    )
 578
 579                else:
 580                    iList[iType][ticker]["step"] = 0  # hack to avoid empty value in some instruments, e.g. futures
 581
 582        return iList
 583
 584    def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None:
 585        """
 586        Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.
 587
 588        See also: `DumpInstruments()`, `Listing()`.
 589
 590        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
 591                            otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) .
 592        """
 593        if self.iListDumpFile is None or not self.iListDumpFile:
 594            uLogger.error("Output name of dump file must be defined!")
 595            raise Exception("Filename required")
 596
 597        if not self.iList or forceUpdate:
 598            self.iList = self.Listing()
 599
 600        xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx"
 601
 602        # Save as XLSX with separated sheets for every type of instruments:
 603        with pd.ExcelWriter(
 604                path=xlsxDumpFile,
 605                date_format=TKS_DATE_FORMAT,
 606                datetime_format=TKS_DATE_TIME_FORMAT,
 607                mode="w",
 608        ) as writer:
 609            for iType in TKS_INSTRUMENTS:
 610                df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index")  # generate pandas object from self.iList dictionary
 611                df = df[sorted(df)]  # sorted by column names
 612                df = df.applymap(
 613                    lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item,
 614                    na_action="ignore",
 615                )  # converting numbers from nano-type to float in every cell
 616                df.to_excel(
 617                    writer,
 618                    sheet_name=iType,
 619                    encoding="UTF-8",
 620                    freeze_panes=(1, 1),
 621                )  # saving as XLSX-file with freeze first row and column as headers
 622
 623        uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile)))
 624
 625    def DumpInstruments(self, forceUpdate: bool = True) -> str:
 626        """
 627        Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server
 628        using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file.
 629
 630        See also: `DumpInstrumentsAsXLSX()`, `Listing()`.
 631
 632        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
 633                            otherwise just saves exist `iList` as JSON-file (default: `dump.json`).
 634        :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file.
 635        """
 636        if self.iListDumpFile is None or not self.iListDumpFile:
 637            uLogger.error("Output name of dump file must be defined!")
 638            raise Exception("Filename required")
 639
 640        if not self.iList or forceUpdate:
 641            self.iList = self.Listing()
 642
 643        jsonDump = json.dumps(self.iList, indent=4, sort_keys=False)  # create JSON object as string
 644        with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH:
 645            fH.write(jsonDump)
 646
 647        uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile)))
 648
 649        return jsonDump
 650
 651    def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str:
 652        """
 653        Show information about one instrument defined by json data and prints it in Markdown format.
 654
 655        See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`.
 656
 657        :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self._ticker]`
 658        :param show: if `True` then also printing information about instrument and its current price.
 659        :return: multilines text in Markdown format with information about one instrument.
 660        """
 661        splitLine = "|                                                             |                                                        |\n"
 662        infoText = ""
 663
 664        if iJSON is not None and iJSON and isinstance(iJSON, dict):
 665            info = [
 666                "# Main information\n\n",
 667                "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
 668                "| Parameters                                                  | Values                                                 |\n",
 669                "|-------------------------------------------------------------|--------------------------------------------------------|\n",
 670                "| Ticker:                                                     | {:<54} |\n".format(iJSON["ticker"]),
 671                "| Full name:                                                  | {:<54} |\n".format(iJSON["name"]),
 672            ]
 673
 674            if "sector" in iJSON.keys() and iJSON["sector"]:
 675                info.append("| Sector:                                                     | {:<54} |\n".format(iJSON["sector"]))
 676
 677            if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] and "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"]:
 678                info.append("| Country of instrument:                                      | {:<54} |\n".format("({}) {}".format(iJSON["countryOfRisk"], iJSON["countryOfRiskName"])))
 679
 680            info.extend([
 681                splitLine,
 682                "| FIGI (Financial Instrument Global Identifier):              | {:<54} |\n".format(iJSON["figi"]),
 683                "| Real exchange [Exchange section]:                           | {:<54} |\n".format("{} [{}]".format(TKS_REAL_EXCHANGES[iJSON["realExchange"]], iJSON["exchange"])),
 684            ])
 685
 686            if "isin" in iJSON.keys() and iJSON["isin"]:
 687                info.append("| ISIN (International Securities Identification Number):      | {:<54} |\n".format(iJSON["isin"]))
 688
 689            if "classCode" in iJSON.keys():
 690                info.append("| Class Code (exchange section where instrument is traded):   | {:<54} |\n".format(iJSON["classCode"]))
 691
 692            info.extend([
 693                splitLine,
 694                "| Current broker security trading status:                     | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]),
 695                splitLine,
 696                "| Buy operations allowed:                                     | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"),
 697                "| Sale operations allowed:                                    | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"),
 698                "| Short positions allowed:                                    | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"),
 699            ])
 700
 701            if iJSON["figi"]:
 702                self._figi = iJSON["figi"]
 703                iJSON = iJSON | self.RequestTradingStatus()
 704
 705                info.extend([
 706                    splitLine,
 707                    "| Limit orders allowed:                                       | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"),
 708                    "| Market orders allowed:                                      | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"),
 709                    "| API trade allowed:                                          | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"),
 710                ])
 711
 712            info.append(splitLine)
 713
 714            if "type" in iJSON.keys() and iJSON["type"]:
 715                info.append("| Type of the instrument:                                     | {:<54} |\n".format(iJSON["type"]))
 716
 717                if "shareType" in iJSON.keys() and iJSON["shareType"]:
 718                    info.append("| Share type:                                                 | {:<54} |\n".format(TKS_SHARE_TYPES[iJSON["shareType"]]))
 719
 720            if "futuresType" in iJSON.keys() and iJSON["futuresType"]:
 721                info.append("| Futures type:                                               | {:<54} |\n".format(iJSON["futuresType"]))
 722
 723            if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]:
 724                info.append("| IPO date:                                                   | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", "")))
 725
 726            if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]:
 727                info.append("| Released date:                                              | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", "")))
 728
 729            if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]:
 730                info.append("| Rebalancing frequency:                                      | {:<54} |\n".format(iJSON["rebalancingFreq"]))
 731
 732            if "focusType" in iJSON.keys() and iJSON["focusType"]:
 733                info.append("| Focusing type:                                              | {:<54} |\n".format(iJSON["focusType"]))
 734
 735            if "assetType" in iJSON.keys() and iJSON["assetType"]:
 736                info.append("| Asset type:                                                 | {:<54} |\n".format(iJSON["assetType"]))
 737
 738            if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]:
 739                info.append("| Basic asset:                                                | {:<54} |\n".format(iJSON["basicAsset"]))
 740
 741            if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]:
 742                info.append("| Basic asset size:                                           | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"]))))
 743
 744            if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]:
 745                info.append("| ISO currency name:                                          | {:<54} |\n".format(iJSON["isoCurrencyName"]))
 746
 747            if "currency" in iJSON.keys():
 748                info.append("| Payment currency:                                           | {:<54} |\n".format(iJSON["currency"]))
 749
 750            if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys():
 751                info.append("| Nominal currency:                                           | {:<54} |\n".format(iJSON["nominal"]["currency"]))
 752
 753            if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]:
 754                info.append("| First trade date:                                           | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", "")))
 755
 756            if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]:
 757                info.append("| Last trade date:                                            | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", "")))
 758
 759            if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]:
 760                info.append("| Date of expiration:                                         | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", "")))
 761
 762            if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]:
 763                info.append("| State registration date:                                    | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", "")))
 764
 765            if "placementDate" in iJSON.keys() and iJSON["placementDate"]:
 766                info.append("| Placement date:                                             | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", "")))
 767
 768            if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]:
 769                info.append("| Maturity date:                                              | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", "")))
 770
 771            if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]:
 772                info.append("| Perpetual bond:                                             | Yes                                                    |\n")
 773
 774            if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]:
 775                info.append("| Over-the-counter (OTC) securities:                          | Yes                                                    |\n")
 776
 777            iExt = None
 778            if iJSON["type"] == "Bonds":
 779                info.extend([
 780                    splitLine,
 781                    "| Bond issue (size / plan):                                   | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])),
 782                    "| Nominal price (100%):                                       | {:<54} |\n".format("{} {}".format(
 783                        "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."),
 784                        iJSON["nominal"]["currency"],
 785                    )),
 786                ])
 787
 788                if "floatingCouponFlag" in iJSON.keys():
 789                    info.append("| Floating coupon:                                            | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No"))
 790
 791                if "amortizationFlag" in iJSON.keys():
 792                    info.append("| Amortization:                                               | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No"))
 793
 794                info.append(splitLine)
 795
 796                if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]:
 797                    info.append("| Number of coupon payments per year:                         | {:<54} |\n".format(iJSON["couponQuantityPerYear"]))
 798
 799                if iJSON["figi"]:
 800                    iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False)  # extended bonds data
 801
 802                    info.extend([
 803                        "| Days last to maturity date:                                 | {:<54} |\n".format(iExt["daysToMaturity"][0]),
 804                        "| Coupons yield (average coupon daily yield * 365):           | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])),
 805                        "| Current price yield (average daily yield * 365):            | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])),
 806                    ])
 807
 808                if "aciValue" in iJSON.keys() and iJSON["aciValue"]:
 809                    info.append("| Current accumulated coupon income (ACI):                    | {:<54} |\n".format("{:.2f} {}".format(
 810                        NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]),
 811                        iJSON["aciValue"]["currency"]
 812                    )))
 813
 814            if "currentPrice" in iJSON.keys():
 815                info.append(splitLine)
 816
 817                currency = iJSON["currency"] if "currency" in iJSON.keys() else ""  # nominal currency for bonds, otherwise currency of instrument
 818                aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else ""  # payment currency
 819
 820                bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0  # previous close price of bond
 821                bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0  # last price of bond
 822                bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0  # max price of bond
 823                bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0  # min price of bond
 824                bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0  # delta between last deal price and last close
 825
 826                curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0
 827                curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0
 828
 829                info.extend([
 830                    "| Previous close price of the instrument:                     | {:<54} |\n".format("{}{}".format(
 831                        "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A",
 832                        "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
 833                    )),
 834                    "| Last deal price of the instrument:                          | {:<54} |\n".format("{}{}".format(
 835                        "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A",
 836                        "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
 837                    )),
 838                    "| Changes between last deal price and last close              | {:<54} |\n".format(
 839                        "{:.2f}%{}".format(
 840                            iJSON["currentPrice"]["changes"],
 841                            " ({}{:.2f} {})".format(
 842                                "+" if bondChangesDelta > 0 else "",
 843                                bondChangesDelta,
 844                                aciCurrency
 845                            ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format(
 846                                "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "",
 847                                iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"],
 848                                currency
 849                            ),
 850                        )
 851                    ),
 852                    "| Current limit price, min / max:                             | {:<54} |\n".format("{}{} / {}{}{}".format(
 853                        "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A",
 854                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 855                        "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A",
 856                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 857                        " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else ""
 858                    )),
 859                    "| Actual price, sell / buy:                                   | {:<54} |\n".format("{}{} / {}{}{}".format(
 860                        "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A",
 861                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 862                        "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A",
 863                        "%" if iJSON["type"] == "Bonds" else" {}".format(currency),
 864                        " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else ""
 865                    )),
 866                ])
 867
 868            if "lot" in iJSON.keys():
 869                info.append("| Minimum lot to buy:                                         | {:<54} |\n".format(iJSON["lot"]))
 870
 871            if "step" in iJSON.keys() and iJSON["step"] != 0:
 872                info.append("| Minimum price increment (step):                             | {:<54} |\n".format("{} {}".format(iJSON["step"], iJSON["currency"] if "currency" in iJSON.keys() else "")))
 873
 874            # Add bond payment calendar:
 875            if iJSON["type"] == "Bonds":
 876                strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False)   # bond payment calendar
 877                info.extend(["\n#", strCalendar])
 878
 879            infoText += "".join(info)
 880
 881            if show:
 882                uLogger.info("{}".format(infoText))
 883
 884            else:
 885                uLogger.debug("{}".format(infoText))
 886
 887            if self.infoFile is not None:
 888                with open(self.infoFile, "w", encoding="UTF-8") as fH:
 889                    fH.write(infoText)
 890
 891                uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile)))
 892
 893                if self.useHTMLReports:
 894                    htmlFilePath = self.infoFile.replace(".md", ".html") if self.infoFile.endswith(".md") else self.infoFile + ".html"
 895                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
 896                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Main information", commonCSS=COMMON_CSS, markdown=infoText))
 897
 898                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
 899
 900        return infoText
 901
 902    def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict:
 903        """
 904        Search and return raw broker's information about instrument by its ticker. Variable `ticker` must be defined!
 905
 906        :param requestPrice: if `False` then do not request current price of instrument (because this is long operation).
 907        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
 908        :return: JSON formatted data with information about instrument.
 909        """
 910        tickerJSON = {}
 911        if self.moreDebug:
 912            uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self._ticker))
 913
 914        if not self._ticker:
 915            uLogger.warning("self._ticker variable is not be empty!")
 916
 917        else:
 918            if self._ticker in TKS_TICKERS_OR_FIGI_EXCLUDED:
 919                uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self._ticker))
 920                raise Exception("Instrument not allowed")
 921
 922            if not self.iList:
 923                self.iList = self.Listing()
 924
 925            if self._ticker in self.iList["Shares"].keys():
 926                tickerJSON = self.iList["Shares"][self._ticker]
 927                if self.moreDebug:
 928                    uLogger.debug("Ticker [{}] found in shares list".format(self._ticker))
 929
 930            elif self._ticker in self.iList["Currencies"].keys():
 931                tickerJSON = self.iList["Currencies"][self._ticker]
 932                if self.moreDebug:
 933                    uLogger.debug("Ticker [{}] found in currencies list".format(self._ticker))
 934
 935            elif self._ticker in self.iList["Bonds"].keys():
 936                tickerJSON = self.iList["Bonds"][self._ticker]
 937                if self.moreDebug:
 938                    uLogger.debug("Ticker [{}] found in bonds list".format(self._ticker))
 939
 940            elif self._ticker in self.iList["Etfs"].keys():
 941                tickerJSON = self.iList["Etfs"][self._ticker]
 942                if self.moreDebug:
 943                    uLogger.debug("Ticker [{}] found in etfs list".format(self._ticker))
 944
 945            elif self._ticker in self.iList["Futures"].keys():
 946                tickerJSON = self.iList["Futures"][self._ticker]
 947                if self.moreDebug:
 948                    uLogger.debug("Ticker [{}] found in futures list".format(self._ticker))
 949
 950        if tickerJSON:
 951            self._figi = tickerJSON["figi"]
 952
 953            if requestPrice:
 954                tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False)
 955
 956                if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None:
 957                    tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"]
 958
 959                else:
 960                    tickerJSON["currentPrice"]["changes"] = 0
 961
 962            if show:
 963                self.ShowInstrumentInfo(iJSON=tickerJSON, show=True)  # print info as Markdown text
 964
 965        else:
 966            if show:
 967                uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self._ticker))
 968
 969        return tickerJSON
 970
 971    def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict:
 972        """
 973        Search and return raw broker's information about instrument by its FIGI. Variable `figi` must be defined!
 974
 975        :param requestPrice: if `False` then do not request current price of instrument (it's long operation).
 976        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
 977        :return: JSON formatted data with information about instrument.
 978        """
 979        figiJSON = {}
 980        if self.moreDebug:
 981            uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self._figi))
 982
 983        if not self._figi:
 984            uLogger.warning("self._figi variable is not be empty!")
 985
 986        else:
 987            if self._figi in TKS_TICKERS_OR_FIGI_EXCLUDED:
 988                uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self._figi))
 989                raise Exception("Instrument not allowed")
 990
 991            if not self.iList:
 992                self.iList = self.Listing()
 993
 994            for item in self.iList["Shares"].keys():
 995                if self._figi == self.iList["Shares"][item]["figi"]:
 996                    figiJSON = self.iList["Shares"][item]
 997
 998                    if self.moreDebug:
 999                        uLogger.debug("FIGI [{}] found in shares list".format(self._figi))
1000
1001                    break
1002
1003            if not figiJSON:
1004                for item in self.iList["Currencies"].keys():
1005                    if self._figi == self.iList["Currencies"][item]["figi"]:
1006                        figiJSON = self.iList["Currencies"][item]
1007
1008                        if self.moreDebug:
1009                            uLogger.debug("FIGI [{}] found in currencies list".format(self._figi))
1010
1011                        break
1012
1013            if not figiJSON:
1014                for item in self.iList["Bonds"].keys():
1015                    if self._figi == self.iList["Bonds"][item]["figi"]:
1016                        figiJSON = self.iList["Bonds"][item]
1017
1018                        if self.moreDebug:
1019                            uLogger.debug("FIGI [{}] found in bonds list".format(self._figi))
1020
1021                        break
1022
1023            if not figiJSON:
1024                for item in self.iList["Etfs"].keys():
1025                    if self._figi == self.iList["Etfs"][item]["figi"]:
1026                        figiJSON = self.iList["Etfs"][item]
1027
1028                        if self.moreDebug:
1029                            uLogger.debug("FIGI [{}] found in etfs list".format(self._figi))
1030
1031                        break
1032
1033            if not figiJSON:
1034                for item in self.iList["Futures"].keys():
1035                    if self._figi == self.iList["Futures"][item]["figi"]:
1036                        figiJSON = self.iList["Futures"][item]
1037
1038                        if self.moreDebug:
1039                            uLogger.debug("FIGI [{}] found in futures list".format(self._figi))
1040
1041                        break
1042
1043        if figiJSON:
1044            self._figi = figiJSON["figi"]
1045            self._ticker = figiJSON["ticker"]
1046
1047            if requestPrice:
1048                figiJSON["currentPrice"] = self.GetCurrentPrices(show=False)
1049
1050                if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None:
1051                    figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"]
1052
1053                else:
1054                    figiJSON["currentPrice"]["changes"] = 0
1055
1056            if show:
1057                self.ShowInstrumentInfo(iJSON=figiJSON, show=True)  # print info as Markdown text
1058
1059        else:
1060            if show:
1061                uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self._figi))
1062
1063        return figiJSON
1064
1065    def GetCurrentPrices(self, show: bool = True) -> dict:
1066        """
1067        Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5:
1068        `{"buy": [{"price": 1243.8, "quantity": 193},
1069                  {"price": 1244.0, "quantity": 168},
1070                  {"price": 1244.8, "quantity": 5},
1071                  {"price": 1245.0, "quantity": 61},
1072                  {"price": 1245.4, "quantity": 60}],
1073          "sell": [{"price": 1243.6, "quantity": 8},
1074                   {"price": 1242.6, "quantity": 10},
1075                   {"price": 1242.4, "quantity": 18},
1076                   {"price": 1242.2, "quantity": 50},
1077                   {"price": 1242.0, "quantity": 113}],
1078          "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean:
1079        - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order
1080        - sell: list of dicts with Buyers prices,
1081            - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument),
1082            - quantity: volume value by current price in lots,
1083        - limitUp: current trade session limit price, maximum,
1084        - limitDown: current trade session limit price, minimum,
1085        - lastPrice: last deal price of the instrument,
1086        - closePrice: previous trade session close price of the instrument.
1087
1088        See also: `SearchByTicker()` and `SearchByFIGI()`.
1089        REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1090        Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1091
1092        :param show: if `True` then print DOM to log and console.
1093        :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`.
1094                 If an error occurred then returns an empty record:
1095                 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`.
1096        """
1097        prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0}
1098
1099        if self.depth < 1:
1100            uLogger.error("Depth of Market (DOM) must be >=1!")
1101            raise Exception("Incorrect value")
1102
1103        if not (self._ticker or self._figi):
1104            uLogger.error("self._ticker or self._figi variables must be defined!")
1105            raise Exception("Ticker or FIGI required")
1106
1107        if self._ticker and not self._figi:
1108            instrumentByTicker = self.SearchByTicker(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1109            self._figi = instrumentByTicker["figi"] if instrumentByTicker else ""
1110
1111        if not self._ticker and self._figi:
1112            instrumentByFigi = self.SearchByFIGI(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1113            self._ticker = instrumentByFigi["ticker"] if instrumentByFigi else ""
1114
1115        if not self._figi:
1116            uLogger.error("FIGI is not defined!")
1117            raise Exception("Ticker or FIGI required")
1118
1119        else:
1120            uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self._ticker, self._figi))
1121
1122            # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1123            priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook"
1124            self.body = str({"figi": self._figi, "depth": self.depth})
1125            pricesResponse = self.SendAPIRequest(priceURL, reqType="POST")  # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1126
1127            if pricesResponse and not ("code" in pricesResponse.keys() or "message" in pricesResponse.keys() or "description" in pricesResponse.keys()):
1128                # list of dicts with sellers orders:
1129                prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]]
1130
1131                # list of dicts with buyers orders:
1132                prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]]
1133
1134                # max price of instrument at this time:
1135                prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None
1136
1137                # min price of instrument at this time:
1138                prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None
1139
1140                # last price of deal with instrument:
1141                prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0
1142
1143                # last close price of instrument:
1144                prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0
1145
1146            else:
1147                uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi))
1148                uLogger.debug("Server response: {}".format(pricesResponse))
1149
1150            if show:
1151                if prices["buy"] or prices["sell"]:
1152                    info = [
1153                        "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format(
1154                            datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
1155                            self._ticker,
1156                            self._figi,
1157                            self.depth,
1158                        ),
1159                        "-" * 60, "\n",
1160                        "             Orders of Buyers | Orders of Sellers\n",
1161                        "-" * 60, "\n",
1162                        "        Sell prices (volumes) | Buy prices (volumes)\n",
1163                        "-" * 60, "\n",
1164                    ]
1165
1166                    if not prices["buy"]:
1167                        info.append("                              | No orders!\n")
1168                        sumBuy = 0
1169
1170                    else:
1171                        sumBuy = sum([x["quantity"] for x in prices["buy"]])
1172                        maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True)
1173                        for item in maxMinSorted:
1174                            info.append("                              | {} ({})\n".format(item["price"], item["quantity"]))
1175
1176                    if not prices["sell"]:
1177                        info.append("No orders!                    |\n")
1178                        sumSell = 0
1179
1180                    else:
1181                        sumSell = sum([x["quantity"] for x in prices["sell"]])
1182                        for item in prices["sell"]:
1183                            info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"])))
1184
1185                    info.extend([
1186                        "-" * 60, "\n",
1187                        "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)),
1188                        "-" * 60, "\n",
1189                    ])
1190
1191                    infoText = "".join(info)
1192
1193                    uLogger.info("Current prices in order book:\n\n{}".format(infoText))
1194
1195                else:
1196                    uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi))
1197
1198        return prices
1199
1200    def ShowInstrumentsInfo(self, show: bool = True) -> str:
1201        """
1202        This method get and show information about all available broker instruments for current user account.
1203        If `instrumentsFile` string is not empty then also save information to this file.
1204
1205        :param show: if `True` then print results to console, if `False` — print only to file.
1206        :return: multi-lines string with all available broker instruments
1207        """
1208        if not self.iList:
1209            self.iList = self.Listing()
1210
1211        info = [
1212            "# All available instruments from Tinkoff Broker server for current user token\n\n",
1213            "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
1214        ]
1215
1216        # add instruments count by type:
1217        for iType in self.iList.keys():
1218            info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType])))
1219
1220        headerLine = "| Ticker       | Full name                                                 | FIGI         | Cur | Lot     | Step       |\n"
1221        splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n"
1222
1223        # generating info tables with all instruments by type:
1224        for iType in self.iList.keys():
1225            info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine])
1226
1227            for instrument in self.iList[iType].keys():
1228                iName = self.iList[iType][instrument]["name"]  # instrument's name
1229                if len(iName) > 57:
1230                    iName = "{}...".format(iName[:54])  # right trim for a long string
1231
1232                info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format(
1233                    self.iList[iType][instrument]["ticker"],
1234                    iName,
1235                    self.iList[iType][instrument]["figi"],
1236                    self.iList[iType][instrument]["currency"],
1237                    self.iList[iType][instrument]["lot"],
1238                    "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0,
1239                ))
1240
1241        infoText = "".join(info)
1242
1243        if show:
1244            uLogger.info(infoText)
1245
1246        if self.instrumentsFile:
1247            with open(self.instrumentsFile, "w", encoding="UTF-8") as fH:
1248                fH.write(infoText)
1249
1250            uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile)))
1251
1252            if self.useHTMLReports:
1253                htmlFilePath = self.instrumentsFile.replace(".md", ".html") if self.instrumentsFile.endswith(".md") else self.instrumentsFile + ".html"
1254                with open(htmlFilePath, "w", encoding="UTF-8") as fH:
1255                    fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="List of instruments", commonCSS=COMMON_CSS, markdown=infoText))
1256
1257                uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
1258
1259        return infoText
1260
1261    def SearchInstruments(self, pattern: str, show: bool = True) -> dict:
1262        """
1263        This method search and show information about instruments by part of its ticker, FIGI or name.
1264        If `searchResultsFile` string is not empty then also save information to this file.
1265
1266        :param pattern: string with part of ticker, FIGI or instrument's name.
1267        :param show: if `True` then print results to console, if `False` — return list of result only.
1268        :return: list of dictionaries with all found instruments.
1269        """
1270        if not self.iList:
1271            self.iList = self.Listing()
1272
1273        searchResults = {iType: {} for iType in self.iList}  # same as iList but will contain only filtered instruments
1274        compiledPattern = re.compile(pattern, re.IGNORECASE)
1275
1276        for iType in self.iList:
1277            for instrument in self.iList[iType].values():
1278                searchResult = compiledPattern.search(" ".join(
1279                    [instrument["ticker"], instrument["figi"], instrument["name"]]
1280                ))
1281
1282                if searchResult:
1283                    searchResults[iType][instrument["ticker"]] = instrument
1284
1285        resultsLen = sum([len(searchResults[iType]) for iType in searchResults])
1286        info = [
1287            "# Search results\n\n",
1288            "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
1289            "* **Search pattern:** [{}]\n".format(pattern),
1290            "* **Found instruments:** [{}]\n\n".format(resultsLen),
1291            '**Note:** you can view info about found instruments with key "--info", e.g.: "tksbrokerapi -t TICKER --info" or "tksbrokerapi -f FIGI --info".\n'
1292        ]
1293        infoShort = info[:]
1294
1295        headerLine = "| Type       | Ticker       | Full name                                                      | FIGI         |\n"
1296        splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n"
1297        skippedLine = "| ...        | ...          | ...                                                            | ...          |\n"
1298
1299        if resultsLen == 0:
1300            info.append("\nNo results\n")
1301            infoShort.append("\nNo results\n")
1302            uLogger.warning("No results. Try changing your search pattern.")
1303
1304        else:
1305            for iType in searchResults:
1306                iTypeValuesCount = len(searchResults[iType].values())
1307                if iTypeValuesCount > 0:
1308                    info.extend(["\n## {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1309                    infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1310
1311                    for instrument in searchResults[iType].values():
1312                        info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format(
1313                            instrument["type"],
1314                            instrument["ticker"],
1315                            "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"],  # right trim for a long string
1316                            instrument["figi"],
1317                        ))
1318
1319                    if iTypeValuesCount <= 5:
1320                        infoShort.extend(info[-iTypeValuesCount:])
1321
1322                    else:
1323                        infoShort.extend(info[-5:])
1324                        infoShort.append(skippedLine)
1325
1326        infoText = "".join(info)
1327        infoTextShort = "".join(infoShort)
1328
1329        if show:
1330            uLogger.info(infoTextShort)
1331            uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`")
1332
1333        if self.searchResultsFile:
1334            with open(self.searchResultsFile, "w", encoding="UTF-8") as fH:
1335                fH.write(infoText)
1336
1337            uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile)))
1338
1339            if self.useHTMLReports:
1340                htmlFilePath = self.searchResultsFile.replace(".md", ".html") if self.searchResultsFile.endswith(".md") else self.searchResultsFile + ".html"
1341                with open(htmlFilePath, "w", encoding="UTF-8") as fH:
1342                    fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Search results", commonCSS=COMMON_CSS, markdown=infoText))
1343
1344                uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
1345
1346        return searchResults
1347
1348    def GetUniqueFIGIs(self, instruments: list[str]) -> list:
1349        """
1350        Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs.
1351
1352        :param instruments: list of strings with tickers or FIGIs.
1353        :return: list with unique instrument FIGIs only.
1354        """
1355        requestedInstruments = []
1356        for iName in instruments:
1357            if iName not in self.aliases.keys():
1358                if iName not in requestedInstruments:
1359                    requestedInstruments.append(iName)
1360
1361            else:
1362                if iName not in requestedInstruments:
1363                    if self.aliases[iName] not in requestedInstruments:
1364                        requestedInstruments.append(self.aliases[iName])
1365
1366        uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments))
1367
1368        onlyUniqueFIGIs = []
1369        for iName in requestedInstruments:
1370            if iName in TKS_TICKERS_OR_FIGI_EXCLUDED:
1371                continue
1372
1373            self._ticker = iName
1374            iData = self.SearchByTicker(requestPrice=False)  # trying to find instrument by ticker
1375
1376            if not iData:
1377                self._ticker = ""
1378                self._figi = iName
1379
1380                iData = self.SearchByFIGI(requestPrice=False)  # trying to find instrument by FIGI
1381
1382                if not iData:
1383                    self._figi = ""
1384                    uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName))
1385
1386            if iData and iData["figi"] not in onlyUniqueFIGIs:
1387                onlyUniqueFIGIs.append(iData["figi"])
1388
1389        uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs))
1390
1391        return onlyUniqueFIGIs
1392
1393    def GetListOfPrices(self, instruments: list[str], show: bool = False) -> list[dict]:
1394        """
1395        This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation!
1396
1397        See limits: https://tinkoff.github.io/investAPI/limits/
1398
1399        If `pricesFile` string is not empty then also save information to this file.
1400
1401        :param instruments: list of strings with tickers or FIGIs.
1402        :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`.
1403        :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1404                 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods.
1405        """
1406        if instruments is None or not instruments:
1407            uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!")
1408            raise Exception("Ticker or FIGI required")
1409
1410        onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments)
1411
1412        uLogger.debug("Requesting current prices from Tinkoff Broker server...")
1413
1414        iList = []  # trying to get info and current prices about all unique instruments:
1415        for self._figi in onlyUniqueFIGIs:
1416            iData = self.SearchByFIGI(requestPrice=True)
1417            iList.append(iData)
1418
1419        self.ShowListOfPrices(iList, show)
1420
1421        return iList
1422
1423    def ShowListOfPrices(self, iList: list, show: bool = True) -> str:
1424        """
1425        Show table contains current prices of given instruments.
1426
1427        :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1428                      One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods.
1429        :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`.
1430        :return: multilines text in Markdown format as a table contains current prices.
1431        """
1432        infoText = ""
1433
1434        if show or self.pricesFile:
1435            info = [
1436                "# Current prices\n\n* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
1437                "| Ticker       | FIGI         | Type       | Prev. close | Last price  | Chg. %   | Day limits min/max  | Actual sell / buy   | Curr. |\n",
1438                "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n",
1439            ]
1440
1441            for item in iList:
1442                info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format(
1443                    item["ticker"],
1444                    item["figi"],
1445                    item["type"],
1446                    "{:.2f}".format(float(item["currentPrice"]["closePrice"])),
1447                    "{:.2f}".format(float(item["currentPrice"]["lastPrice"])),
1448                    "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])),
1449                    "{} / {}".format(
1450                        item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A",
1451                        item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A",
1452                    ),
1453                    "{} / {}".format(
1454                        item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A",
1455                        item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A",
1456                    ),
1457                    item["currency"],
1458                ))
1459
1460            infoText = "".join(info)
1461
1462            if show:
1463                uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText))
1464
1465            if self.pricesFile:
1466                with open(self.pricesFile, "w", encoding="UTF-8") as fH:
1467                    fH.write(infoText)
1468
1469                uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile)))
1470
1471                if self.useHTMLReports:
1472                    htmlFilePath = self.pricesFile.replace(".md", ".html") if self.pricesFile.endswith(".md") else self.pricesFile + ".html"
1473                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
1474                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Current prices", commonCSS=COMMON_CSS, markdown=infoText))
1475
1476                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
1477
1478        return infoText
1479
1480    def RequestTradingStatus(self) -> dict:
1481        """
1482        Requesting trading status for the instrument defined by `figi` variable.
1483
1484        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus
1485
1486        Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest
1487
1488        :return: dictionary with trading status attributes. Response example:
1489                 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING",
1490                  "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}`
1491        """
1492        if self._figi is None or not self._figi:
1493            uLogger.error("Variable `figi` must be defined for using this method!")
1494            raise Exception("FIGI required")
1495
1496        uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self._figi))
1497
1498        self.body = str({"figi": self._figi, "instrumentId": self._figi})
1499        tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus"
1500        tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST")
1501
1502        if self.moreDebug:
1503            uLogger.debug("Records about current trading status successfully received")
1504
1505        return tradingStatus
1506
1507    def RequestPortfolio(self) -> dict:
1508        """
1509        Requesting actual user's portfolio for current `accountId`.
1510
1511        REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio
1512
1513        Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest
1514
1515        :return: dictionary with user's portfolio.
1516        """
1517        if self.accountId is None or not self.accountId:
1518            uLogger.error("Variable `accountId` must be defined for using this method!")
1519            raise Exception("Account ID required")
1520
1521        uLogger.debug("Requesting current actual user's portfolio. Wait, please...")
1522
1523        self.body = str({"accountId": self.accountId})
1524        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio"
1525        rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST")
1526
1527        if self.moreDebug:
1528            uLogger.debug("Records about user's portfolio successfully received")
1529
1530        return rawPortfolio
1531
1532    def RequestPositions(self) -> dict:
1533        """
1534        Requesting open positions by currencies and instruments for current `accountId`.
1535
1536        REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions
1537
1538        Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest
1539
1540        :return: dictionary with open positions by instruments.
1541        """
1542        if self.accountId is None or not self.accountId:
1543            uLogger.error("Variable `accountId` must be defined for using this method!")
1544            raise Exception("Account ID required")
1545
1546        uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...")
1547
1548        self.body = str({"accountId": self.accountId})
1549        positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions"
1550        rawPositions = self.SendAPIRequest(positionsURL, reqType="POST")
1551
1552        if self.moreDebug:
1553            uLogger.debug("Records about current open positions successfully received")
1554
1555        return rawPositions
1556
1557    def RequestPendingOrders(self) -> list:
1558        """
1559        Requesting current actual pending limit orders for current `accountId`.
1560
1561        REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders
1562
1563        Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest
1564
1565        :return: list of dictionaries with pending limit orders.
1566        """
1567        if self.accountId is None or not self.accountId:
1568            uLogger.error("Variable `accountId` must be defined for using this method!")
1569            raise Exception("Account ID required")
1570
1571        uLogger.debug("Requesting current actual pending limit orders. Wait, please...")
1572
1573        self.body = str({"accountId": self.accountId})
1574        ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders"
1575        rawResponse = self.SendAPIRequest(ordersURL, reqType="POST")
1576
1577        if "orders" in rawResponse.keys():
1578            rawOrders = rawResponse["orders"]
1579            uLogger.debug("[{}] records about pending limit orders received".format(len(rawOrders)))
1580
1581        else:
1582            rawOrders = []
1583            uLogger.debug("No pending limit orders returned! rawResponse = {}".format(rawResponse))
1584
1585        return rawOrders
1586
1587    def RequestStopOrders(self) -> list:
1588        """
1589        Requesting current actual stop orders for current `accountId`.
1590
1591        REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders
1592
1593        Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest
1594
1595        :return: list of dictionaries with stop orders.
1596        """
1597        if self.accountId is None or not self.accountId:
1598            uLogger.error("Variable `accountId` must be defined for using this method!")
1599            raise Exception("Account ID required")
1600
1601        uLogger.debug("Requesting current actual stop orders. Wait, please...")
1602
1603        self.body = str({"accountId": self.accountId})
1604        stopOrdersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders"
1605        rawResponse = self.SendAPIRequest(stopOrdersURL, reqType="POST")
1606
1607        if "stopOrders" in rawResponse.keys():
1608            rawStopOrders = rawResponse["stopOrders"]
1609            uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders)))
1610
1611        else:
1612            rawStopOrders = []
1613            uLogger.debug("No stop orders returned! rawResponse = {}".format(rawResponse))
1614
1615        return rawStopOrders
1616
1617    def Overview(self, show: bool = False, details: str = "full") -> dict:
1618        """
1619        Get portfolio: all open positions, orders and some statistics for current `accountId`.
1620        If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile`
1621        and `overviewBondsCalendarFile` are defined then also save information to file.
1622
1623        WARNING! It is not recommended to run this method too many times in a loop! The server receives
1624        many requests about the state of the portfolio, and then, based on the received data, a large number
1625        of calculation and statistics are collected.
1626
1627        :param show: if `False` then only dictionary returns, if `True` then show more debug information.
1628        :param details: how detailed should the information be?
1629        - `full` — shows full available information about portfolio status (by default),
1630        - `positions` — shows only open positions,
1631        - `orders` — shows only sections of open limits and stop orders.
1632        - `digest` — show a short digest of the portfolio status,
1633        - `analytics` — shows only the analytics section and the distribution of the portfolio by various categories,
1634        - `calendar` — shows only the bonds calendar section (if these present in portfolio),
1635        :return: dictionary with client's raw portfolio and some statistics.
1636        """
1637        if self.accountId is None or not self.accountId:
1638            uLogger.error("Variable `accountId` must be defined for using this method!")
1639            raise Exception("Account ID required")
1640
1641        view = {
1642            "raw": {  # --- raw portfolio responses from broker with user portfolio data:
1643                "headers": {},  # list of dictionaries, response headers without "positions" section
1644                "Currencies": [],  # list of dictionaries, open trades with currencies from "positions" section
1645                "Shares": [],  # list of dictionaries, open trades with shares from "positions" section
1646                "Bonds": [],  # list of dictionaries, open trades with bonds from "positions" section
1647                "Etfs": [],  # list of dictionaries, open trades with etfs from "positions" section
1648                "Futures": [],  # list of dictionaries, open trades with futures from "positions" section
1649                "positions": {},  # raw response from broker: dictionary with current available or blocked currencies and instruments for client
1650                "orders": [],  # raw response from broker: list of dictionaries with all pending (market) orders
1651                "stopOrders": [],  # raw response from broker: list of dictionaries with all stop orders
1652                "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}},  # dict with prices of all currencies in RUB
1653            },
1654            "stat": {  # --- some statistics calculated using "raw" sections:
1655                "portfolioCostRUB": 0.,  # portfolio cost in RUB (Russian Rouble)
1656                "availableRUB": 0.,  # available rubles (without other currencies)
1657                "blockedRUB": 0.,  # blocked sum in Russian Rouble
1658                "totalChangesRUB": 0.,  # changes for all open trades in RUB
1659                "totalChangesPercentRUB": 0.,  # changes for all open trades in percents
1660                "allCurrenciesCostRUB": 0.,  # costs of all currencies (include rubles) in RUB
1661                "sharesCostRUB": 0.,  # costs of all shares in RUB
1662                "bondsCostRUB": 0.,  # costs of all bonds in RUB
1663                "etfsCostRUB": 0.,  # costs of all etfs in RUB
1664                "futuresCostRUB": 0.,  # costs of all futures in RUB
1665                "Currencies": [],  # list of dictionaries of all currencies statistics
1666                "Shares": [],  # list of dictionaries of all shares statistics
1667                "Bonds": [],  # list of dictionaries of all bonds statistics
1668                "Etfs": [],  # list of dictionaries of all etfs statistics
1669                "Futures": [],  # list of dictionaries of all futures statistics
1670                "orders": [],  # list of dictionaries of all pending (market) orders and it's parameters
1671                "stopOrders": [],  # list of dictionaries of all stop orders and it's parameters
1672                "blockedCurrencies": {},  # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21}
1673                "blockedInstruments": {},  # dict with blocked  by FIGI, e.g. {}
1674                "funds": {},  # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1675            },
1676            "analytics": {  # --- some analytics of portfolio:
1677                "distrByAssets": {},  # portfolio distribution by assets
1678                "distrByCompanies": {},  # portfolio distribution by companies
1679                "distrBySectors": {},  # portfolio distribution by sectors
1680                "distrByCurrencies": {},  # portfolio distribution by currencies
1681                "distrByCountries": {},  # portfolio distribution by countries
1682                "bondsCalendar": None,  # bonds payment calendar as Pandas DataFrame (if these present in portfolio)
1683            }
1684        }
1685
1686        details = details.lower()
1687        availableDetails = ["full", "positions", "orders", "analytics", "calendar", "digest"]
1688        if details not in availableDetails:
1689            details = "full"
1690            uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails))
1691
1692        uLogger.debug("Requesting portfolio of a client. Wait, please...")
1693
1694        portfolioResponse = self.RequestPortfolio()  # current user's portfolio (dict)
1695        view["raw"]["positions"] = self.RequestPositions()  # current open positions by instruments (dict)
1696        view["raw"]["orders"] = self.RequestPendingOrders()  # current actual pending limit orders (list)
1697        view["raw"]["stopOrders"] = self.RequestStopOrders()  # current actual stop orders (list)
1698
1699        # save response headers without "positions" section:
1700        for key in portfolioResponse.keys():
1701            if key != "positions":
1702                view["raw"]["headers"][key] = portfolioResponse[key]
1703
1704            else:
1705                continue
1706
1707        # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation
1708        # Type of instrument must be only one of supported types in TKS_INSTRUMENTS
1709        for item in portfolioResponse["positions"]:
1710            if item["instrumentType"] == "currency":
1711                self._figi = item["figi"]
1712                if not self._figi and item["ticker"]:
1713                    self._ticker = item["ticker"]
1714                    self._figi = self.SearchByTicker()["figi"]  # Get FIGI to avoid warnings
1715
1716                curr = self.SearchByFIGI(requestPrice=False)
1717
1718                # current price of currency in RUB:
1719                view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = {
1720                    "name": curr["name"],
1721                    "currentPrice": NanoToFloat(
1722                        item["currentPrice"]["units"],
1723                        item["currentPrice"]["nano"]
1724                    ),
1725                }
1726
1727                view["raw"]["Currencies"].append(item)
1728
1729            elif item["instrumentType"] == "share":
1730                view["raw"]["Shares"].append(item)
1731
1732            elif item["instrumentType"] == "bond":
1733                view["raw"]["Bonds"].append(item)
1734
1735            elif item["instrumentType"] == "etf":
1736                view["raw"]["Etfs"].append(item)
1737
1738            elif item["instrumentType"] == "futures":
1739                view["raw"]["Futures"].append(item)
1740
1741            else:
1742                continue
1743
1744        # how many volume of currencies (by ISO currency name) are blocked:
1745        for item in view["raw"]["positions"]["blocked"]:
1746            blocked = NanoToFloat(item["units"], item["nano"])
1747            if blocked > 0:
1748                view["stat"]["blockedCurrencies"][item["currency"]] = blocked
1749
1750        # how many volume of instruments (by FIGI) are blocked:
1751        for item in view["raw"]["positions"]["securities"]:
1752            blocked = int(item["blocked"])
1753            if blocked > 0:
1754                view["stat"]["blockedInstruments"][item["figi"]] = blocked
1755
1756        allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]}
1757
1758        if "rub" in allBlocked.keys():
1759            view["stat"]["blockedRUB"] = allBlocked["rub"]  # blocked rubles
1760
1761        # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies:
1762        view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"])
1763        view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"])
1764        view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"])
1765        view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"])
1766        view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"])
1767        view["stat"]["portfolioCostRUB"] = sum([
1768            view["stat"]["allCurrenciesCostRUB"],
1769            view["stat"]["sharesCostRUB"],
1770            view["stat"]["bondsCostRUB"],
1771            view["stat"]["etfsCostRUB"],
1772            view["stat"]["futuresCostRUB"],
1773        ])
1774
1775        # --- calculating some portfolio statistics:
1776        byComp = {}  # distribution by companies
1777        bySect = {}  # distribution by sectors
1778        byCurr = {}  # distribution by currencies (include RUB)
1779        unknownCountryName = "All other countries"  # default name for instruments without "countryOfRisk" and "countryOfRiskName"
1780        byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}}  # distribution by countries (currencies are included in their countries)
1781
1782        for item in portfolioResponse["positions"]:
1783            self._figi = item["figi"]
1784            if not self._figi and item["ticker"]:
1785                self._ticker = item["ticker"]
1786                self._figi = self.SearchByTicker()["figi"]  # Get FIGI to avoid warnings
1787
1788            instrument = self.SearchByFIGI(requestPrice=False)  # full raw info about instrument by FIGI
1789
1790            if instrument:
1791                if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys():
1792                    blocked = allBlocked[instrument["nominal"]["currency"]]  # blocked volume of currency
1793
1794                elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys():
1795                    blocked = allBlocked[item["figi"]]  # blocked volume of other instruments
1796
1797                else:
1798                    blocked = 0
1799
1800                volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"])  # available volume of instrument
1801                lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"])  # available volume in lots of instrument
1802                direction = "Long" if lots >= 0 else "Short"  # direction of an instrument's position: short or long
1803                curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"])  # current instrument's price
1804                average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"])  # current average position price
1805                profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"])  # expected profit at current moment
1806                currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"]  # currency name rub, usd, eur etc.
1807                cost = curPrice if "currentNkd" not in item.keys() else (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume  # current cost of all volume of instrument in basic asset
1808                baseCurrencyName = item["currentPrice"]["currency"]  # name of base currency (rub)
1809                countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName
1810                costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"]  # cost in rubles
1811                percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.  # instrument's part in percent of full portfolio cost
1812
1813                statData = {
1814                    "figi": item["figi"],  # FIGI from REST API "GetPortfolio" method
1815                    "ticker": instrument["ticker"],  # ticker by FIGI
1816                    "currency": currency,  # currency name rub, usd, eur etc. for instrument price
1817                    "volume": volume,  # available volume of instrument
1818                    "lots": lots,  # volume in lots of instrument
1819                    "direction": direction,  # direction of an instrument's position: short or long
1820                    "blocked": blocked,  # blocked volume of currency or instrument
1821                    "currentPrice": curPrice,  # current instrument's price in basic asset
1822                    "average": average,  # current average position price
1823                    "cost": cost,  # current cost of all volume of instrument in basic asset
1824                    "baseCurrencyName": baseCurrencyName,  # name of base currency (rub)
1825                    "costRUB": costRUB,  # cost of instrument in ruble
1826                    "percentCostRUB": percentCostRUB,  # instrument's part in percent of full portfolio cost in RUB
1827                    "profit": profit,  # expected profit at current moment
1828                    "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0,  # expected percents of profit at current moment for this instrument
1829                    "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other",
1830                    "name": instrument["name"] if "name" in instrument.keys() else "",  # human-readable names of instruments
1831                    "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "",  # ISO name for currencies only
1832                    "country": countryName,  # e.g. "[RU] Российская Федерация" or unknownCountryName
1833                    "step": instrument["step"],  # minimum price increment
1834                }
1835
1836                # adding distribution by unique countries:
1837                if statData["country"] not in byCountry.keys():
1838                    byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB}
1839
1840                else:
1841                    byCountry[statData["country"]]["cost"] += costRUB
1842                    byCountry[statData["country"]]["percent"] += percentCostRUB
1843
1844                if item["instrumentType"] != "currency":
1845                    # adding distribution by unique companies:
1846                    if statData["name"]:
1847                        if statData["name"] not in byComp.keys():
1848                            byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB}
1849
1850                        else:
1851                            byComp[statData["name"]]["cost"] += costRUB
1852                            byComp[statData["name"]]["percent"] += percentCostRUB
1853
1854                    # adding distribution by unique sectors:
1855                    if statData["sector"] not in bySect.keys():
1856                        bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB}
1857
1858                    else:
1859                        bySect[statData["sector"]]["cost"] += costRUB
1860                        bySect[statData["sector"]]["percent"] += percentCostRUB
1861
1862                # adding distribution by unique currencies:
1863                if currency not in byCurr.keys():
1864                    byCurr[currency] = {
1865                        "name": view["raw"]["currenciesCurrentPrices"][currency]["name"],
1866                        "cost": costRUB,
1867                        "percent": percentCostRUB
1868                    }
1869
1870                else:
1871                    byCurr[currency]["cost"] += costRUB
1872                    byCurr[currency]["percent"] += percentCostRUB
1873
1874                # saving statistics for every instrument:
1875                if item["instrumentType"] == "currency":
1876                    view["stat"]["Currencies"].append(statData)
1877
1878                    # update dict with free funds for trading (total - blocked) by currencies
1879                    # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1880                    view["stat"]["funds"][currency] = {
1881                        "total": volume,
1882                        "totalCostRUB": costRUB,  # total volume cost in rubles
1883                        "free": volume - blocked,
1884                        "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0,  # free volume cost in rubles
1885                    }
1886
1887                elif item["instrumentType"] == "share":
1888                    view["stat"]["Shares"].append(statData)
1889
1890                elif item["instrumentType"] == "bond":
1891                    view["stat"]["Bonds"].append(statData)
1892
1893                elif item["instrumentType"] == "etf":
1894                    view["stat"]["Etfs"].append(statData)
1895
1896                elif item["instrumentType"] == "Futures":
1897                    view["stat"]["Futures"].append(statData)
1898
1899                else:
1900                    continue
1901
1902        # total changes in Russian Ruble:
1903        view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]])  # available RUB without other currencies
1904        view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0.
1905        startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100)
1906        view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost
1907        view["stat"]["funds"]["rub"] = {
1908            "total": view["stat"]["availableRUB"],
1909            "totalCostRUB": view["stat"]["availableRUB"],
1910            "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1911            "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1912        }
1913
1914        # --- pending limit orders sector data:
1915        uniquePendingOrdersFIGIs = []  # unique FIGIs of pending limit orders to avoid many times price requests
1916        uniquePendingOrders = {}  # unique instruments with FIGIs as dictionary keys
1917
1918        for item in view["raw"]["orders"]:
1919            self._figi = item["figi"]
1920
1921            if item["figi"] not in uniquePendingOrdersFIGIs:
1922                instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI, price requests only one time
1923
1924                uniquePendingOrdersFIGIs.append(item["figi"])
1925                uniquePendingOrders[item["figi"]] = instrument
1926
1927            else:
1928                instrument = uniquePendingOrders[item["figi"]]
1929
1930            if instrument:
1931                action = TKS_ORDER_DIRECTIONS[item["direction"]]
1932                orderType = TKS_ORDER_TYPES[item["orderType"]]
1933                orderState = TKS_ORDER_STATES[item["executionReportStatus"]]
1934                orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1935
1936                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1937                if item["direction"] == "ORDER_DIRECTION_BUY":
1938                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1939
1940                else:
1941                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
1942
1943                # requested price for order execution:
1944                target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"])
1945
1946                # necessary changes in percent to reach target from current price:
1947                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
1948
1949                view["stat"]["orders"].append({
1950                    "orderID": item["orderId"],  # orderId number parameter of current order
1951                    "figi": item["figi"],  # FIGI identification
1952                    "ticker": instrument["ticker"],  # ticker name by FIGI
1953                    "lotsRequested": item["lotsRequested"],  # requested lots value
1954                    "lotsExecuted": item["lotsExecuted"],  # how many lots are executed
1955                    "currentPrice": lastPrice,  # current instrument's price for defined action
1956                    "targetPrice": target,  # requested price for order execution in base currency
1957                    "baseCurrencyName": item["initialSecurityPrice"]["currency"],  # name of base currency
1958                    "percentChanges": changes,  # changes in percent to target from current price
1959                    "currency": item["currency"],  # instrument's currency name
1960                    "action": action,  # sell / buy / Unknown from TKS_ORDER_DIRECTIONS
1961                    "type": orderType,  # type of order from TKS_ORDER_TYPES
1962                    "status": orderState,  # order status from TKS_ORDER_STATES
1963                    "date": orderDate,  # string with order date and time from UTC format (without nano seconds part)
1964                })
1965
1966        # --- stop orders sector data:
1967        uniqueStopOrdersFIGIs = []  # unique FIGIs of stop orders to avoid many times price requests
1968        uniqueStopOrders = {}  # unique instruments with FIGIs as dictionary keys
1969
1970        for item in view["raw"]["stopOrders"]:
1971            self._figi = item["figi"]
1972
1973            if item["figi"] not in uniqueStopOrdersFIGIs:
1974                instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI, price requests only one time
1975
1976                uniqueStopOrdersFIGIs.append(item["figi"])
1977                uniqueStopOrders[item["figi"]] = instrument
1978
1979            else:
1980                instrument = uniqueStopOrders[item["figi"]]
1981
1982            if instrument:
1983                action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]]
1984                orderType = TKS_STOP_ORDER_TYPES[item["orderType"]]
1985                createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1986
1987                # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order
1988                if "expirationTime" in item.keys():
1989                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"]
1990                    expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0]
1991
1992                else:
1993                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"]
1994                    expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"]
1995
1996                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1997                if item["direction"] == "STOP_ORDER_DIRECTION_BUY":
1998                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1999
2000                else:
2001                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
2002
2003                # requested price when stop-order executed:
2004                target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"])
2005
2006                # price for limit-order, set up when stop-order executed:
2007                limit = NanoToFloat(item["price"]["units"], item["price"]["nano"])
2008
2009                # necessary changes in percent to reach target from current price:
2010                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
2011
2012                view["stat"]["stopOrders"].append({
2013                    "orderID": item["stopOrderId"],  # stopOrderId number parameter of current stop-order
2014                    "figi": item["figi"],  # FIGI identification
2015                    "ticker": instrument["ticker"],  # ticker name by FIGI
2016                    "lotsRequested": item["lotsRequested"],  # requested lots value
2017                    "currentPrice": lastPrice,  # current instrument's price for defined action
2018                    "targetPrice": target,  # requested price for stop-order execution in base currency
2019                    "limitPrice": limit,  # price for limit-order, set up when stop-order executed, 0 if market order
2020                    "baseCurrencyName": item["stopPrice"]["currency"],  # name of base currency
2021                    "percentChanges": changes,  # changes in percent to target from current price
2022                    "currency": item["currency"],  # instrument's currency name
2023                    "action": action,  # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS
2024                    "type": orderType,  # type of order from TKS_STOP_ORDER_TYPES
2025                    "expType": expType,  # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES
2026                    "createDate": createDate,  # string with created order date and time from UTC format (without nano seconds part)
2027                    "expDate": expDate,  # string with expiration order date and time from UTC format (without nano seconds part)
2028                })
2029
2030        # --- calculating data for analytics section:
2031        # portfolio distribution by assets:
2032        view["analytics"]["distrByAssets"] = {
2033            "Ruble": {
2034                "uniques": 1,
2035                "cost": view["stat"]["availableRUB"],
2036                "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2037            },
2038            "Currencies": {
2039                "uniques": len(view["stat"]["Currencies"]),  # all foreign currencies without RUB
2040                "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"],
2041                "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2042            },
2043            "Shares": {
2044                "uniques": len(view["stat"]["Shares"]),
2045                "cost": view["stat"]["sharesCostRUB"],
2046                "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2047            },
2048            "Bonds": {
2049                "uniques": len(view["stat"]["Bonds"]),
2050                "cost": view["stat"]["bondsCostRUB"],
2051                "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2052            },
2053            "Etfs": {
2054                "uniques": len(view["stat"]["Etfs"]),
2055                "cost": view["stat"]["etfsCostRUB"],
2056                "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2057            },
2058            "Futures": {
2059                "uniques": len(view["stat"]["Futures"]),
2060                "cost": view["stat"]["futuresCostRUB"],
2061                "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2062            },
2063        }
2064
2065        # portfolio distribution by companies:
2066        view["analytics"]["distrByCompanies"]["All money cash"] = {
2067            "ticker": "",
2068            "cost": view["stat"]["allCurrenciesCostRUB"],
2069            "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2070        }
2071        view["analytics"]["distrByCompanies"].update(byComp)
2072
2073        # portfolio distribution by sectors:
2074        view["analytics"]["distrBySectors"]["All money cash"] = {
2075            "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"],
2076            "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"],
2077        }
2078        view["analytics"]["distrBySectors"].update(bySect)
2079
2080        # portfolio distribution by currencies:
2081        if "rub" not in view["analytics"]["distrByCurrencies"].keys():
2082            view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0}
2083
2084            if self.moreDebug:
2085                uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!")
2086
2087        view["analytics"]["distrByCurrencies"].update(byCurr)
2088        view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2089        view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2090
2091        # portfolio distribution by countries:
2092        if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys():
2093            view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0}
2094
2095            if self.moreDebug:
2096                uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!")
2097
2098        view["analytics"]["distrByCountries"].update(byCountry)
2099        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2100        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2101
2102        # --- Prepare text statistics overview in human-readable:
2103        if show:
2104            actualOnDate = datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)
2105
2106            # Whatever the value `details`, header not changes:
2107            info = [
2108                "# Client's portfolio\n\n",
2109                "* **Actual on date:** [{} UTC]\n".format(actualOnDate),
2110                "* **Account ID:** [{}]\n".format(self.accountId),
2111            ]
2112
2113            if details in ["full", "positions", "digest"]:
2114                info.extend([
2115                    "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2116                    "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format(
2117                        "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2118                        view["stat"]["totalChangesRUB"],
2119                        "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2120                        view["stat"]["totalChangesPercentRUB"],
2121                    ),
2122                ])
2123
2124            if details in ["full", "positions"]:
2125                info.extend([
2126                    "## Open positions\n\n",
2127                    "| Ticker [FIGI]               | Volume (blocked)                | Lots     | Curr. price  | Avg. price   | Current volume cost | Profit (%)                   |\n",
2128                    "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n",
2129                    "| **Ruble:**                  | {:>31} |          |              |              |                     |                              |\n".format(
2130                        "{:.2f} ({:.2f}) rub".format(
2131                            view["stat"]["availableRUB"],
2132                            view["stat"]["blockedRUB"],
2133                        )
2134                    )
2135                ])
2136
2137                def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list:
2138                    return [
2139                        "|                             |                                 |          |              |              |                     |                              |\n",
2140                        "| {:<27} |                                 |          |              |              | {:>19} |                              |\n".format(
2141                            noTradeStr if noTradeStr else typeStr,
2142                            "" if noTradeStr else "{:.2f} RUB".format(CostRUB),
2143                        ),
2144                    ]
2145
2146                def _InfoStr(data: dict, isCurr: bool = False) -> str:
2147                    return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format(
2148                        "{} [{}]".format(data["ticker"], data["figi"]),
2149                        "{:.2f} ({:.2f}) {}".format(
2150                            data["volume"],
2151                            data["blocked"],
2152                            data["currency"],
2153                        ) if isCurr else "{:.0f} ({:.0f})".format(
2154                            data["volume"],
2155                            data["blocked"],
2156                        ),
2157                        "—" if isCurr else "{:.4f}".format(data["lots"]).rstrip("0").rstrip("."),
2158                        "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a",
2159                        "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a",
2160                        "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]),
2161                        "{}{:.2f} {} ({}{:.2f}%)".format(
2162                            "+" if data["profit"] > 0 else "",
2163                            data["profit"], data["baseCurrencyName"],
2164                            "+" if data["percentProfit"] > 0 else "",
2165                            data["percentProfit"],
2166                        ),
2167                    )
2168
2169                # --- Show currencies section:
2170                if view["stat"]["Currencies"]:
2171                    info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**"))
2172                    for item in view["stat"]["Currencies"]:
2173                        info.append(_InfoStr(item, isCurr=True))
2174
2175                else:
2176                    info.extend(_SplitStr(noTradeStr="**Currencies:** no trades"))
2177
2178                # --- Show shares section:
2179                if view["stat"]["Shares"]:
2180                    info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**"))
2181
2182                    for item in view["stat"]["Shares"]:
2183                        info.append(_InfoStr(item))
2184
2185                else:
2186                    info.extend(_SplitStr(noTradeStr="**Shares:** no trades"))
2187
2188                # --- Show bonds section:
2189                if view["stat"]["Bonds"]:
2190                    info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**"))
2191
2192                    for item in view["stat"]["Bonds"]:
2193                        info.append(_InfoStr(item))
2194
2195                else:
2196                    info.extend(_SplitStr(noTradeStr="**Bonds:** no trades"))
2197
2198                # --- Show etfs section:
2199                if view["stat"]["Etfs"]:
2200                    info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**"))
2201
2202                    for item in view["stat"]["Etfs"]:
2203                        info.append(_InfoStr(item))
2204
2205                else:
2206                    info.extend(_SplitStr(noTradeStr="**Etfs:** no trades"))
2207
2208                # --- Show futures section:
2209                if view["stat"]["Futures"]:
2210                    info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**"))
2211
2212                    for item in view["stat"]["Futures"]:
2213                        info.append(_InfoStr(item))
2214
2215                else:
2216                    info.extend(_SplitStr(noTradeStr="**Futures:** no trades"))
2217
2218            if details in ["full", "orders"]:
2219                # --- Show pending limit orders section:
2220                if view["stat"]["orders"]:
2221                    info.extend([
2222                        "\n## Opened pending limit-orders: [{}]\n".format(len(view["stat"]["orders"])),
2223                        "\n| Ticker [FIGI]               | Order ID       | Lots (exec.) | Current price (% delta) | Target price  | Action    | Type      | Create date (UTC)       |\n",
2224                        "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n",
2225                    ])
2226
2227                    for item in view["stat"]["orders"]:
2228                        info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format(
2229                            "{} [{}]".format(item["ticker"], item["figi"]),
2230                            item["orderID"],
2231                            "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]),
2232                            "{} {} ({}{:.2f}%)".format(
2233                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2234                                item["baseCurrencyName"],
2235                                "+" if item["percentChanges"] > 0 else "",
2236                                float(item["percentChanges"]),
2237                            ),
2238                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2239                            item["action"],
2240                            item["type"],
2241                            item["date"],
2242                        ))
2243
2244                else:
2245                    info.append("\n## Total pending limit-orders: [0]\n")
2246
2247                # --- Show stop orders section:
2248                if view["stat"]["stopOrders"]:
2249                    info.extend([
2250                        "\n## Opened stop-orders: [{}]\n".format(len(view["stat"]["stopOrders"])),
2251                        "\n| Ticker [FIGI]               | Stop order ID                        | Lots   | Current price (% delta) | Target price  | Limit price   | Action    | Type        | Expire type  | Create date (UTC)   | Expiration (UTC)    |\n",
2252                        "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n",
2253                    ])
2254
2255                    for item in view["stat"]["stopOrders"]:
2256                        info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format(
2257                            "{} [{}]".format(item["ticker"], item["figi"]),
2258                            item["orderID"],
2259                            item["lotsRequested"],
2260                            "{} {} ({}{:.2f}%)".format(
2261                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2262                                item["baseCurrencyName"],
2263                                "+" if item["percentChanges"] > 0 else "",
2264                                float(item["percentChanges"]),
2265                            ),
2266                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2267                            "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"],
2268                            item["action"],
2269                            item["type"],
2270                            item["expType"],
2271                            item["createDate"],
2272                            item["expDate"],
2273                        ))
2274
2275                else:
2276                    info.append("\n## Total stop-orders: [0]\n")
2277
2278            if details in ["full", "analytics"]:
2279                # -- Show analytics section:
2280                if view["stat"]["portfolioCostRUB"] > 0:
2281                    info.extend([
2282                        "\n# Analytics\n\n"
2283                        "* **Actual on date:** [{} UTC]\n".format(actualOnDate),
2284                        "* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2285                        "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format(
2286                            "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2287                            view["stat"]["totalChangesRUB"],
2288                            "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2289                            view["stat"]["totalChangesPercentRUB"],
2290                        ),
2291                        "\n## Portfolio distribution by assets\n"
2292                        "\n| Type                               | Uniques | Percent | Current cost       |\n",
2293                        "|------------------------------------|---------|---------|--------------------|\n",
2294                    ])
2295
2296                    for key in view["analytics"]["distrByAssets"].keys():
2297                        if view["analytics"]["distrByAssets"][key]["cost"] > 0:
2298                            info.append("| {:<34} | {:<7} | {:<7} | {:<18} |\n".format(
2299                                key,
2300                                view["analytics"]["distrByAssets"][key]["uniques"],
2301                                "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]),
2302                                "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]),
2303                            ))
2304
2305                    aSepLine = "|----------------------------------------------|---------|--------------------|\n"
2306
2307                    info.extend([
2308                        "\n## Portfolio distribution by companies\n"
2309                        "\n| Company                                      | Percent | Current cost       |\n",
2310                        aSepLine,
2311                    ])
2312
2313                    for company in view["analytics"]["distrByCompanies"].keys():
2314                        if view["analytics"]["distrByCompanies"][company]["cost"] > 0:
2315                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2316                                "{}{}".format(
2317                                    "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "",
2318                                    company,
2319                                ),
2320                                "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]),
2321                                "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]),
2322                            ))
2323
2324                    info.extend([
2325                        "\n## Portfolio distribution by sectors\n"
2326                        "\n| Sector                                       | Percent | Current cost       |\n",
2327                        aSepLine,
2328                    ])
2329
2330                    for sector in view["analytics"]["distrBySectors"].keys():
2331                        if view["analytics"]["distrBySectors"][sector]["cost"] > 0:
2332                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2333                                sector,
2334                                "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]),
2335                                "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]),
2336                            ))
2337
2338                    info.extend([
2339                        "\n## Portfolio distribution by currencies\n"
2340                        "\n| Instruments currencies                       | Percent | Current cost       |\n",
2341                        aSepLine,
2342                    ])
2343
2344                    for curr in view["analytics"]["distrByCurrencies"].keys():
2345                        if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0:
2346                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2347                                "[{}] {}".format(curr, view["analytics"]["distrByCurrencies"][curr]["name"]),
2348                                "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]),
2349                                "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]),
2350                            ))
2351
2352                    info.extend([
2353                        "\n## Portfolio distribution by countries\n"
2354                        "\n| Assets by country                            | Percent | Current cost       |\n",
2355                        aSepLine,
2356                    ])
2357
2358                    for country in view["analytics"]["distrByCountries"].keys():
2359                        if view["analytics"]["distrByCountries"][country]["cost"] > 0:
2360                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2361                                country,
2362                                "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]),
2363                                "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]),
2364                            ))
2365
2366            if details in ["full", "calendar"]:
2367                # -- Show bonds payment calendar section:
2368                if view["stat"]["Bonds"]:
2369                    bondTickers = [item["ticker"] for item in view["stat"]["Bonds"]]
2370                    view["analytics"]["bondsCalendar"] = self.ExtendBondsData(instruments=bondTickers, xlsx=False)
2371                    info.append("\n" + self.ShowBondsCalendar(extBonds=view["analytics"]["bondsCalendar"], show=False))
2372
2373                else:
2374                    info.append("\n# Bond payments calendar\n\nNo bonds in the portfolio to create payments calendar\n")
2375
2376            infoText = "".join(info)
2377
2378            uLogger.info(infoText)
2379
2380            if details == "full" and self.overviewFile:
2381                filename = self.overviewFile
2382
2383            elif details == "digest" and self.overviewDigestFile:
2384                filename = self.overviewDigestFile
2385
2386            elif details == "positions" and self.overviewPositionsFile:
2387                filename = self.overviewPositionsFile
2388
2389            elif details == "orders" and self.overviewOrdersFile:
2390                filename = self.overviewOrdersFile
2391
2392            elif details == "analytics" and self.overviewAnalyticsFile:
2393                filename = self.overviewAnalyticsFile
2394
2395            elif details == "calendar" and self.overviewBondsCalendarFile:
2396                filename = self.overviewBondsCalendarFile
2397
2398            else:
2399                filename = ""
2400
2401            if filename:
2402                with open(filename, "w", encoding="UTF-8") as fH:
2403                    fH.write(infoText)
2404
2405                uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename)))
2406
2407                if self.useHTMLReports:
2408                    htmlFilePath = filename.replace(".md", ".html") if filename.endswith(".md") else filename + ".html"
2409                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
2410                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Client's portfolio", commonCSS=COMMON_CSS, markdown=infoText))
2411
2412                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
2413
2414        return view
2415
2416    def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple[list[dict], dict]:
2417        """
2418        Returns history operations between two given dates for current `accountId`.
2419        If `reportFile` string is not empty then also save human-readable report.
2420        Shows some statistical data of closed positions.
2421
2422        :param start: see docstring in `TradeRoutines.GetDatesAsString()` method.
2423        :param end: see docstring in `TradeRoutines.GetDatesAsString()` method.
2424        :param show: if `True` then also prints all records to the console.
2425        :param showCancelled: if `False` then remove information about cancelled operations from the deals report.
2426        :return: original list of dictionaries with history of deals records from API ("operations" key):
2427                 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2428                 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.
2429        """
2430        if self.accountId is None or not self.accountId:
2431            uLogger.error("Variable `accountId` must be defined for using this method!")
2432            raise Exception("Account ID required")
2433
2434        startDate, endDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT)  # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2435
2436        uLogger.debug("Requesting history of a client's operations. Wait, please...")
2437
2438        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2439        dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations"
2440        self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate})
2441        ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"]  # list of dict: operations returns by broker
2442        customStat = {}  # custom statistics in additional to responseJSON
2443
2444        # --- output report in human-readable format:
2445        if show or self.reportFile:
2446            splitLine1 = "|                            |                               |                              |                      |                        |\n"  # Summary section
2447            splitLine2 = "|                     |              |              |            |           |                 |            |                                                                    |\n"  # Operations section
2448            nextDay = ""
2449
2450            info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])]
2451
2452            if len(ops) > 0:
2453                customStat = {
2454                    "opsCount": 0,  # total operations count
2455                    "buyCount": 0,  # buy operations
2456                    "sellCount": 0,  # sell operations
2457                    "buyTotal": {"rub": 0.},  # Buy sums in different currencies
2458                    "sellTotal": {"rub": 0.},  # Sell sums in different currencies
2459                    "payIn": {"rub": 0.},  # Deposit brokerage account
2460                    "payOut": {"rub": 0.},  # Withdrawals
2461                    "divs": {"rub": 0.},  # Dividends income
2462                    "coupons": {"rub": 0.},  # Coupon's income
2463                    "brokerCom": {"rub": 0.},  # Service commissions
2464                    "serviceCom": {"rub": 0.},  # Service commissions
2465                    "marginCom": {"rub": 0.},  # Margin commissions
2466                    "allTaxes": {"rub": 0.},  # Sum of withholding taxes and corrections
2467                }
2468
2469                # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES:
2470                for item in ops:
2471                    if item["state"] == "OPERATION_STATE_EXECUTED":
2472                        payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2473
2474                        # count buy operations:
2475                        if "_BUY" in item["operationType"]:
2476                            customStat["buyCount"] += 1
2477
2478                            if item["payment"]["currency"] in customStat["buyTotal"].keys():
2479                                customStat["buyTotal"][item["payment"]["currency"]] += payment
2480
2481                            else:
2482                                customStat["buyTotal"][item["payment"]["currency"]] = payment
2483
2484                        # count sell operations:
2485                        elif "_SELL" in item["operationType"]:
2486                            customStat["sellCount"] += 1
2487
2488                            if item["payment"]["currency"] in customStat["sellTotal"].keys():
2489                                customStat["sellTotal"][item["payment"]["currency"]] += payment
2490
2491                            else:
2492                                customStat["sellTotal"][item["payment"]["currency"]] = payment
2493
2494                        # count incoming operations:
2495                        elif item["operationType"] in ["OPERATION_TYPE_INPUT"]:
2496                            if item["payment"]["currency"] in customStat["payIn"].keys():
2497                                customStat["payIn"][item["payment"]["currency"]] += payment
2498
2499                            else:
2500                                customStat["payIn"][item["payment"]["currency"]] = payment
2501
2502                        # count withdrawals operations:
2503                        elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]:
2504                            if item["payment"]["currency"] in customStat["payOut"].keys():
2505                                customStat["payOut"][item["payment"]["currency"]] += payment
2506
2507                            else:
2508                                customStat["payOut"][item["payment"]["currency"]] = payment
2509
2510                        # count dividends income:
2511                        elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]:
2512                            if item["payment"]["currency"] in customStat["divs"].keys():
2513                                customStat["divs"][item["payment"]["currency"]] += payment
2514
2515                            else:
2516                                customStat["divs"][item["payment"]["currency"]] = payment
2517
2518                        # count coupon's income:
2519                        elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]:
2520                            if item["payment"]["currency"] in customStat["coupons"].keys():
2521                                customStat["coupons"][item["payment"]["currency"]] += payment
2522
2523                            else:
2524                                customStat["coupons"][item["payment"]["currency"]] = payment
2525
2526                        # count broker commissions:
2527                        elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]:
2528                            if item["payment"]["currency"] in customStat["brokerCom"].keys():
2529                                customStat["brokerCom"][item["payment"]["currency"]] += payment
2530
2531                            else:
2532                                customStat["brokerCom"][item["payment"]["currency"]] = payment
2533
2534                        # count service commissions:
2535                        elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]:
2536                            if item["payment"]["currency"] in customStat["serviceCom"].keys():
2537                                customStat["serviceCom"][item["payment"]["currency"]] += payment
2538
2539                            else:
2540                                customStat["serviceCom"][item["payment"]["currency"]] = payment
2541
2542                        # count margin commissions:
2543                        elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]:
2544                            if item["payment"]["currency"] in customStat["marginCom"].keys():
2545                                customStat["marginCom"][item["payment"]["currency"]] += payment
2546
2547                            else:
2548                                customStat["marginCom"][item["payment"]["currency"]] = payment
2549
2550                        # count withholding taxes:
2551                        elif "_TAX" in item["operationType"]:
2552                            if item["payment"]["currency"] in customStat["allTaxes"].keys():
2553                                customStat["allTaxes"][item["payment"]["currency"]] += payment
2554
2555                            else:
2556                                customStat["allTaxes"][item["payment"]["currency"]] = payment
2557
2558                        else:
2559                            continue
2560
2561                customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"]
2562
2563                # --- view "Actions" lines:
2564                info.extend([
2565                    "| Report sections            |                               |                              |                      |                        |\n",
2566                    "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n",
2567                    "| **Actions:**               | Trades: {:<21} | Trading volumes:             |                      |                        |\n".format(customStat["opsCount"]),
2568                    "|                            |   Buy: {:<22} | {:<28} |                      |                        |\n".format(
2569                        "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2570                        "  rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else "  —",
2571                    ),
2572                    "|                            |   Sell: {:<21} | {:<28} |                      |                        |\n".format(
2573                        "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2574                        "  rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else "  —",
2575                    ),
2576                ])
2577
2578                opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys()))))
2579                for key in opsKeys:
2580                    if key == "rub":
2581                        continue
2582
2583                    info.extend([
2584                        "|                            |                               | {:<28} |                      |                        |\n".format(
2585                            "  {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0)
2586                        ),
2587                        "|                            |                               | {:<28} |                      |                        |\n".format(
2588                            "  {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0)
2589                        ),
2590                    ])
2591
2592                info.append(splitLine1)
2593
2594                def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str:
2595                    return "|                            | {:<29} | {:<28} | {:<20} | {:<22} |\n".format(
2596                            "  {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else "  —",
2597                            "  {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else "  —",
2598                            "  {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else "  —",
2599                            "  {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else "  —",
2600                    )
2601
2602                # --- view "Payments" lines:
2603                info.append("| **Payments:**              | Deposit on broker account:    | Withdrawals:                 | Dividends income:    | Coupons income:        |\n")
2604                paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys()))))
2605
2606                for key in paymentsKeys:
2607                    info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key))
2608
2609                info.append(splitLine1)
2610
2611                # --- view "Commissions and taxes" lines:
2612                info.append("| **Commissions and taxes:** | Broker commissions:           | Service commissions:         | Margin commissions:  | All taxes/corrections: |\n")
2613                comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys()))))
2614
2615                for key in comKeys:
2616                    info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key))
2617
2618                info.extend([
2619                    "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"),
2620                    "| Date and time       | FIGI         | Ticker       | Asset      | Value     | Payment         | Status     | Operation type                                                     |\n",
2621                    "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n",
2622                ])
2623
2624            else:
2625                info.append("Broker returned no operations during this period\n")
2626
2627            # --- view "Operations" section:
2628            for item in ops:
2629                if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]:
2630                    continue
2631
2632                else:
2633                    self._figi = item["figi"]
2634                    payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2635                    instrument = self.SearchByFIGI(requestPrice=False) if self._figi else {}
2636
2637                    # group of deals during one day:
2638                    if nextDay and item["date"].split("T")[0] != nextDay:
2639                        info.append(splitLine2)
2640                        nextDay = ""
2641
2642                    else:
2643                        nextDay = item["date"].split("T")[0]  # saving current day for splitting
2644
2645                    info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format(
2646                        item["date"].replace("T", " ").replace("Z", "").split(".")[0],
2647                        self._figi if self._figi else "—",
2648                        instrument["ticker"] if instrument else "—",
2649                        instrument["type"] if instrument else "—",
2650                        item["quantity"] if int(item["quantity"]) > 0 else "—",
2651                        "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—",
2652                        TKS_OPERATION_STATES[item["state"]],
2653                        TKS_OPERATION_TYPES[item["operationType"]],
2654                    ))
2655
2656            infoText = "".join(info)
2657
2658            if show:
2659                if self.moreDebug:
2660                    uLogger.debug("Records about history of a client's operations successfully received")
2661
2662                uLogger.info(infoText)
2663
2664            if self.reportFile:
2665                with open(self.reportFile, "w", encoding="UTF-8") as fH:
2666                    fH.write(infoText)
2667
2668                uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile)))
2669
2670                if self.useHTMLReports:
2671                    htmlFilePath = self.reportFile.replace(".md", ".html") if self.reportFile.endswith(".md") else self.reportFile + ".html"
2672                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
2673                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Client's operations", commonCSS=COMMON_CSS, markdown=infoText))
2674
2675                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
2676
2677        return ops, customStat
2678
2679    def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False) -> pd.DataFrame:
2680        """
2681        This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id).
2682
2683        History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`.
2684        Warning! Broker server used ISO UTC time by default.
2685
2686        If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame.
2687        Also, `historyFile` used to update history with `onlyMissing` parameter.
2688
2689        See also: `LoadHistory()` and `ShowHistoryChart()` methods.
2690
2691        :param start: see docstring in `TradeRoutines.GetDatesAsString()` method.
2692        :param end: see docstring in `TradeRoutines.GetDatesAsString()` method.
2693        :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`,
2694                         `"hour"`, `"day"`. Default: `"hour"`.
2695        :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`.
2696                            False by default. Warning! History appends only from last candle to current time
2697                            with always update last candle!
2698        :param csvSep: separator if csv-file is used, `,` by default.
2699        :param show: if `True` then also prints Pandas DataFrame to the console.
2700        :return: Pandas DataFrame with prices history. Headers of columns are defined by default:
2701                 `["date", "time", "open", "high", "low", "close", "volume"]`.
2702        """
2703        strStartDate, strEndDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT)  # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2704        headers = ["date", "time", "open", "high", "low", "close", "volume"]  # sequence and names of column headers
2705        history = None  # empty pandas object for history
2706
2707        if interval not in TKS_CANDLE_INTERVALS.keys():
2708            uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.")
2709            raise Exception("Incorrect value")
2710
2711        if not (self._ticker or self._figi):
2712            uLogger.error("Ticker or FIGI must be defined!")
2713            raise Exception("Ticker or FIGI required")
2714
2715        if self._ticker and not self._figi:
2716            instrumentByTicker = self.SearchByTicker(requestPrice=False)
2717            self._figi = instrumentByTicker["figi"] if instrumentByTicker else ""
2718
2719        if self._figi and not self._ticker:
2720            instrumentByFIGI = self.SearchByFIGI(requestPrice=False)
2721            self._ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else ""
2722
2723        dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from start time string
2724        dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from end time string
2725        if interval.lower() != "day":
2726            dtEnd += timedelta(seconds=1)  # adds 1 sec for requests, because day end returned by `TradeRoutines.GetDatesAsString()` is 23:59:59
2727
2728        delta = dtEnd - dtStart  # current UTC time minus last time in file
2729        deltaMinutes = delta.days * 1440 + delta.seconds // 60  # minutes between start and end dates
2730
2731        # calculate history length in candles:
2732        length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1]
2733        if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0:
2734            length += 1  # to avoid fraction time
2735
2736        # calculate data blocks count:
2737        blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2]
2738
2739        uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end))
2740        uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate))
2741        uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval))
2742        uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2]))
2743        uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self._ticker, self._figi))
2744
2745        tempOld = None  # pandas object for old history, if --only-missing key present
2746        lastTime = None  # datetime object of last old candle in file
2747
2748        if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile):
2749            uLogger.debug("--only-missing key present, add only last missing candles...")
2750            uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile)))
2751
2752            tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers)
2753
2754            tempOld["date"] = pd.to_datetime(tempOld["date"])  # load date "as is"
2755            tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d")  # convert date to string
2756            tempOld["time"] = pd.to_datetime(tempOld["time"])  # load time "as is"
2757            tempOld["time"] = tempOld["time"].dt.strftime("%H:%M")  # convert time to string
2758
2759            # get last datetime object from last string in file or minus 1 delta if file is empty:
2760            if len(tempOld) > 0:
2761                lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2762
2763            else:
2764                lastTime = dtEnd - timedelta(days=1)  # history file is empty, so last date set at -1 day
2765
2766            tempOld = tempOld[:-1]  # always remove last old candle because it may be incompletely at the current time
2767
2768        responseJSONs = []  # raw history blocks of data
2769
2770        blockEnd = dtEnd
2771        for item in range(blocks):
2772            tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2]
2773            blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail)
2774
2775            uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format(
2776                item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2777            ))
2778
2779            if blockStart == blockEnd:
2780                uLogger.debug("Skipped this zero-length block...")
2781
2782            else:
2783                # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles
2784                historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles"
2785                self.body = str({
2786                    "figi": self._figi,
2787                    "from": blockStart.strftime(TKS_DATE_TIME_FORMAT),
2788                    "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2789                    "interval": TKS_CANDLE_INTERVALS[interval][0]
2790                })
2791                responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1)
2792
2793                if "code" in responseJSON.keys():
2794                    uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks))
2795
2796                else:
2797                    if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1:
2798                        responseJSON["candles"] = responseJSON["candles"][:-1]  # removes last candle for "yesterday" request
2799
2800                    responseJSONs = responseJSON["candles"] + responseJSONs  # add more old history behind newest dates
2801
2802            blockEnd = blockStart
2803
2804        printCount = len(responseJSONs)  # candles to show in console
2805        if responseJSONs:
2806            tempHistory = pd.DataFrame(
2807                data={
2808                    "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2809                    "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2810                    "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs],
2811                    "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs],
2812                    "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs],
2813                    "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs],
2814                    "volume": [int(item["volume"]) for item in responseJSONs],
2815                },
2816                index=range(len(responseJSONs)),
2817                columns=["date", "time", "open", "high", "low", "close", "volume"],
2818            )
2819            tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d")
2820            tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M")
2821
2822            # append only newest candles to old history if --only-missing key present:
2823            if onlyMissing and tempOld is not None and lastTime is not None:
2824                index = 0  # find start index in tempHistory data:
2825
2826                for i, item in tempHistory.iterrows():
2827                    curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2828
2829                    if curTime == lastTime:
2830                        uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
2831                        index = i
2832                        printCount = index + 1
2833                        break
2834
2835                history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True)
2836
2837            else:
2838                history = tempHistory  # if no `--only-missing` key then load full data from server
2839
2840            uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False)))
2841
2842        if history is not None and not history.empty:
2843            if show:
2844                uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format(
2845                    strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]),
2846                    pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False),
2847                ))
2848
2849        else:
2850            uLogger.warning("Received an empty candles history!")
2851
2852        if self.historyFile is not None:
2853            if history is not None and not history.empty:
2854                history.to_csv(self.historyFile, sep=csvSep, index=False, header=None)
2855                uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self._ticker, self._figi, interval, os.path.abspath(self.historyFile)))
2856
2857            else:
2858                uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile)))
2859
2860        else:
2861            uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.")
2862
2863        return history
2864
2865    def LoadHistory(self, filePath: str) -> pd.DataFrame:
2866        """
2867        Load candles history from csv-file and return Pandas DataFrame object.
2868
2869        See also: `History()` and `ShowHistoryChart()` methods.
2870
2871        :param filePath: path to csv-file to open.
2872        """
2873        loadedHistory = None  # init candles data object
2874
2875        uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...")
2876
2877        if os.path.exists(filePath):
2878            loadedHistory = self.priceModel.LoadFromFile(filePath)  # load data and get chain of candles as Pandas DataFrame
2879
2880            tfStr = self.priceModel.FormattedDelta(
2881                self.priceModel.timeframe,
2882                "{days} days {hours}h {minutes}m {seconds}s",
2883            ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta(
2884                self.priceModel.timeframe,
2885                "{hours}h {minutes}m {seconds}s",
2886            )
2887
2888            if loadedHistory is not None and not loadedHistory.empty:
2889                uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format(
2890                    len(loadedHistory),
2891                    tfStr,
2892                    pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)),
2893                )
2894
2895            else:
2896                uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath)))
2897
2898        else:
2899            uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath))
2900
2901        return loadedHistory
2902
2903    def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None:
2904        """
2905        Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.
2906
2907        Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart.
2908        Default: `index.html` (both for interact and non-interact candlesticks chart).
2909
2910        See also: `History()` and `LoadHistory()` methods.
2911
2912        :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object.
2913        :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart.
2914                         See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters
2915                         If False then chain of candlesticks will render as not interactive Google Candlestick chart.
2916                         See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template
2917        :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to
2918                              html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file.
2919        """
2920        if isinstance(candles, str):
2921            self.priceModel.prices = self.LoadHistory(filePath=candles)  # load candles chain from file
2922            self.priceModel.ticker = os.path.basename(candles)  # use filename as ticker name in PriceGenerator
2923
2924        elif isinstance(candles, pd.DataFrame):
2925            self.priceModel.prices = candles  # set candles chain from variable
2926            self.priceModel.ticker = self._ticker  # use current TKSBrokerAPI ticker as ticker name in PriceGenerator
2927
2928            if "datetime" not in candles.columns:
2929                self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True)  # PriceGenerator uses "datetime" column with date and time
2930
2931        else:
2932            uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!")
2933            raise Exception("Incorrect value")
2934
2935        self.priceModel.horizon = len(self.priceModel.prices)  # use length of candles data as horizon in PriceGenerator
2936
2937        if interact:
2938            uLogger.debug("Rendering interactive candles chart. Wait, please...")
2939
2940            self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2941
2942        else:
2943            uLogger.debug("Rendering non-interactive candles chart. Wait, please...")
2944
2945            self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2946
2947        uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile)))
2948
2949    def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2950        """
2951        Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response.
2952        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
2953
2954        See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`.
2955
2956        :param operation: string "Buy" or "Sell".
2957        :param lots: volume, integer count of lots >= 1.
2958        :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`.
2959        :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`.
2960        :param expDate: string "Undefined" by default or local date in future,
2961                        it is a string with format `%Y-%m-%d %H:%M:%S`.
2962        :return: JSON with response from broker server.
2963        """
2964        if self.accountId is None or not self.accountId:
2965            uLogger.error("Variable `accountId` must be defined for using this method!")
2966            raise Exception("Account ID required")
2967
2968        if operation is None or not operation or operation not in ("Buy", "Sell"):
2969            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
2970            raise Exception("Incorrect value")
2971
2972        if lots is None or lots < 1:
2973            uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.")
2974            lots = 1
2975
2976        if tp is None or tp < 0:
2977            tp = 0
2978
2979        if sl is None or sl < 0:
2980            sl = 0
2981
2982        if expDate is None or not expDate:
2983            expDate = "Undefined"
2984
2985        if not (self._ticker or self._figi):
2986            uLogger.error("Ticker or FIGI must be defined!")
2987            raise Exception("Ticker or FIGI required")
2988
2989        instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True)
2990        self._ticker = instrument["ticker"]
2991        self._figi = instrument["figi"]
2992
2993        uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self._ticker, self._figi, lots, tp, sl, expDate))
2994
2995        openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
2996        self.body = str({
2997            "figi": self._figi,
2998            "quantity": str(lots),
2999            "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
3000            "accountId": str(self.accountId),
3001            "orderType": "ORDER_TYPE_MARKET",  # see: TKS_ORDER_TYPES
3002        })
3003        response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0)
3004
3005        if "orderId" in response.keys():
3006            uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format(
3007                operation, response["orderId"],
3008                self._ticker, self._figi, lots,
3009                NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"],
3010                NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"],
3011                NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"],
3012            ))
3013
3014            if tp > 0:
3015                self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate)
3016
3017            if sl > 0:
3018                self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate)
3019
3020        else:
3021            uLogger.warning("Not `oK` status received! Market order not executed. See full debug log and try again open order later.")
3022
3023        return response
3024
3025    def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
3026        """
3027        More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response.
3028        If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter.
3029
3030        See also: `Order()` and `Trade()` docstrings.
3031
3032        :param lots: volume, integer count of lots >= 1.
3033        :param tp: float > 0, take profit price of stop-order.
3034        :param sl: float > 0, stop loss price of stop-order.
3035        :param expDate: it's a local date in future.
3036                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3037        :return: JSON with response from broker server.
3038        """
3039        return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate)
3040
3041    def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
3042        """
3043        More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response.
3044        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
3045
3046        See also: `Order()` and `Trade()` docstrings.
3047
3048        :param lots: volume, integer count of lots >= 1.
3049        :param tp: float > 0, take profit price of stop-order.
3050        :param sl: float > 0, stop loss price of stop-order.
3051        :param expDate: it's a local date in the future.
3052                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3053        :return: JSON with response from broker server.
3054        """
3055        return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate)
3056
3057    def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None:
3058        """
3059        Close position of given instruments.
3060
3061        :param instruments: list of instruments defined by tickers or FIGIs that must be closed.
3062        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
3063                         This avoids unnecessary downloading data from the server.
3064        """
3065        if instruments is None or not instruments:
3066            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
3067            raise Exception("Ticker or FIGI required")
3068
3069        if isinstance(instruments, str):
3070            instruments = [instruments]
3071
3072        uniqueInstruments = self.GetUniqueFIGIs(instruments)
3073        if uniqueInstruments:
3074            if portfolio is None or not portfolio:
3075                portfolio = self.Overview(show=False)
3076
3077            allOpened = [item["figi"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]]
3078            uLogger.debug("All opened instruments by it's FIGI: {}".format(", ".join(allOpened)))
3079
3080            for self._figi in uniqueInstruments:
3081                if self._figi not in allOpened:
3082                    uLogger.warning("Instrument with FIGI [{}] not in open positions list!".format(self._figi))
3083                    continue
3084
3085                # search open trade info about instrument by ticker:
3086                instrument = {}
3087                for iType in TKS_INSTRUMENTS:
3088                    if instrument:
3089                        break
3090
3091                    for item in portfolio["stat"][iType]:
3092                        if item["figi"] == self._figi:
3093                            instrument = item
3094                            break
3095
3096                if instrument:
3097                    self._ticker = instrument["ticker"]
3098                    self._figi = instrument["figi"]
3099
3100                    uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format(
3101                        self._ticker,
3102                        self._figi,
3103                        int(instrument["volume"]),
3104                        ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "",
3105                    ))
3106
3107                    tradeLots = abs(instrument["lots"]) - instrument["blocked"]  # available volumes in lots for close operation
3108
3109                    if tradeLots > 0:
3110                        if instrument["blocked"] > 0:
3111                            uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format(
3112                                instrument["blocked"],
3113                                self._ticker,
3114                                tradeLots,
3115                            ))
3116
3117                        # if direction is "Long" then we need sell, if direction is "Short" then we need buy:
3118                        self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots)
3119
3120                    else:
3121                        uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self._ticker))
3122
3123    def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None:
3124        """
3125        Close all positions of given instruments with defined type.
3126
3127        :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
3128        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
3129                         This avoids unnecessary downloading data from the server.
3130        """
3131        if iType not in TKS_INSTRUMENTS:
3132            uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType))
3133
3134        else:
3135            if portfolio is None or not portfolio:
3136                portfolio = self.Overview(show=False)
3137
3138            tickers = [item["ticker"] for item in portfolio["stat"][iType]]
3139            uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers))
3140
3141            if tickers and portfolio:
3142                self.CloseTrades(tickers, portfolio)
3143
3144            else:
3145                uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType))
3146
3147    def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3148        """
3149        Universal method to create market or limit orders with all available parameters for current `accountId`.
3150        See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`.
3151
3152        If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above
3153        current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.
3154
3155        Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell"
3156        then broker immediately open market order as you can do simple --buy or --sell operations!
3157
3158        If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell".
3159        When current price will go up or down to target price value then broker opens a limit order.
3160        Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.
3161
3162        Only one attempt and no retry for opens order. If network issue occurred you can create new request.
3163
3164        :param operation: string "Buy" or "Sell".
3165        :param orderType: string "Limit" or "Stop".
3166        :param lots: volume, integer count of lots >= 1.
3167        :param targetPrice: target price > 0. This is open trade price for limit order.
3168        :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice.
3169                           Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
3170        :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types
3171                         "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3172                         Stop loss order always executed by market price.
3173        :param expDate: string "Undefined" by default or local date in future.
3174                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3175                        This date is converting to UTC format for server. This parameter only makes sense for stop-order.
3176                        A limit order has no expiration date, it lasts until the end of the trading day.
3177        :return: JSON with response from broker server.
3178        """
3179        if self.accountId is None or not self.accountId:
3180            uLogger.error("Variable `accountId` must be defined for using this method!")
3181            raise Exception("Account ID required")
3182
3183        if operation is None or not operation or operation not in ("Buy", "Sell"):
3184            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
3185            raise Exception("Incorrect value")
3186
3187        if orderType is None or not orderType or orderType not in ("Limit", "Stop"):
3188            uLogger.error("You must define order type only one of them: `Limit` or `Stop`!")
3189            raise Exception("Incorrect value")
3190
3191        if lots is None or lots < 1:
3192            uLogger.error("You must define trade volume > 0: integer count of lots!")
3193            raise Exception("Incorrect value")
3194
3195        if targetPrice is None or targetPrice <= 0:
3196            uLogger.error("Target price for limit-order must be greater than 0!")
3197            raise Exception("Incorrect value")
3198
3199        if limitPrice is None or limitPrice <= 0:
3200            limitPrice = targetPrice
3201
3202        if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"):
3203            stopType = "Limit"
3204
3205        if expDate is None or not expDate:
3206            expDate = "Undefined"
3207
3208        if not (self._ticker or self._figi):
3209            uLogger.error("Tocker or FIGI must be defined!")
3210            raise Exception("Ticker or FIGI required")
3211
3212        response = {}
3213        instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True)
3214        self._ticker = instrument["ticker"]
3215        self._figi = instrument["figi"]
3216
3217        if orderType == "Limit":
3218            uLogger.debug(
3219                "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format(
3220                    self._ticker, self._figi,
3221                    operation, lots, targetPrice, instrument["currency"],
3222                ))
3223
3224            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
3225            self.body = str({
3226                "figi": self._figi,
3227                "quantity": str(lots),
3228                "price": FloatToNano(targetPrice),
3229                "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
3230                "accountId": str(self.accountId),
3231                "orderType": "ORDER_TYPE_LIMIT",  # see: TKS_ORDER_TYPES
3232            })
3233            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0)
3234
3235            if "orderId" in response.keys():
3236                uLogger.info(
3237                    "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{} {}]".format(
3238                        response["orderId"], self._ticker, self._figi, operation, lots,
3239                        "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"],
3240                    ))
3241
3242                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3243                    if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]:
3244                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format(
3245                            targetPrice, instrument["currency"],
3246                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3247                        ))
3248
3249                    if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]:
3250                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format(
3251                            targetPrice, instrument["currency"],
3252                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3253                        ))
3254
3255            else:
3256                uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log and try again open order later.")
3257
3258        if orderType == "Stop":
3259            uLogger.debug(
3260                "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format(
3261                    self._ticker, self._figi,
3262                    operation, lots,
3263                    targetPrice, instrument["currency"],
3264                    limitPrice, instrument["currency"],
3265                    stopType, expDate,
3266                ))
3267
3268            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder"
3269            expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT)
3270            stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT"
3271
3272            body = {
3273                "figi": self._figi,
3274                "quantity": str(lots),
3275                "price": FloatToNano(limitPrice),
3276                "stopPrice": FloatToNano(targetPrice),
3277                "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL",  # see: TKS_STOP_ORDER_DIRECTIONS
3278                "accountId": str(self.accountId),
3279                "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL",  # see: TKS_STOP_ORDER_EXPIRATION_TYPES
3280                "stopOrderType": stopOrderType,  # see: TKS_STOP_ORDER_TYPES
3281            }
3282
3283            if expDateUTC:
3284                body["expireDate"] = expDateUTC
3285
3286            self.body = str(body)
3287            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0)
3288
3289            if "stopOrderId" in response.keys():
3290                uLogger.info(
3291                    "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{} {}], limit price [{} {}], stop-order type [{}] and expiration date [{} UTC]".format(
3292                        response["stopOrderId"], self._ticker, self._figi, operation, lots,
3293                        "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"],
3294                        "{:.4f}".format(limitPrice).rstrip("0").rstrip("."), instrument["currency"],
3295                        TKS_STOP_ORDER_TYPES[stopOrderType],
3296                        datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"],
3297                    ))
3298
3299                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3300                    if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3301                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3302                            targetPrice, instrument["currency"],
3303                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3304                        ))
3305
3306                    if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3307                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3308                            targetPrice, instrument["currency"],
3309                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3310                        ))
3311
3312            else:
3313                uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log and try again open order later.")
3314
3315        return response
3316
3317    def BuyLimit(self, lots: int, targetPrice: float) -> dict:
3318        """
3319        Create pending `Buy` limit-order (below current price). You must specify only 2 parameters:
3320        `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then
3321        broker immediately open `Buy` market order, such as if you do simple `--buy` operation!
3322        See also: `Order()` docstring.
3323
3324        :param lots: volume, integer count of lots >= 1.
3325        :param targetPrice: target price > 0. This is open trade price for limit order.
3326        :return: JSON with response from broker server.
3327        """
3328        return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice)
3329
3330    def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3331        """
3332        Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order.
3333        In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3334        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3335        target price value then broker opens a limit order. See also: `Order()` docstring.
3336
3337        :param lots: volume, integer count of lots >= 1.
3338        :param targetPrice: target price > 0. This is trigger price for buy stop-order.
3339        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3340                           with price equal to limitPrice, when current price goes to target price of buy stop-order.
3341        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3342                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3343        :param expDate: string "Undefined" by default or local date in future.
3344                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3345                        This date is converting to UTC format for server.
3346        :return: JSON with response from broker server.
3347        """
3348        return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
3349
3350    def SellLimit(self, lots: int, targetPrice: float) -> dict:
3351        """
3352        Create pending `Sell` limit-order (above current price). You must specify only 2 parameters:
3353        `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then
3354        broker immediately open `Sell` market order, such as if you do simple `--sell` operation!
3355        See also: `Order()` docstring.
3356
3357        :param lots: volume, integer count of lots >= 1.
3358        :param targetPrice: target price > 0. This is open trade price for limit order.
3359        :return: JSON with response from broker server.
3360        """
3361        return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice)
3362
3363    def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3364        """
3365        Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order.
3366        In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3367        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3368        target price value then broker opens a limit order. See also: `Order()` docstring.
3369
3370        :param lots: volume, integer count of lots >= 1.
3371        :param targetPrice: target price > 0. This is trigger price for sell stop-order.
3372        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3373                           with price equal to limitPrice, when current price goes to target price of sell stop-order.
3374        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3375                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3376        :param expDate: string "Undefined" by default or local date in future.
3377                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3378                        This date is converting to UTC format for server.
3379        :return: JSON with response from broker server.
3380        """
3381        return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
3382
3383    def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None:
3384        """
3385        Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`.
3386
3387        :param orderIDs: list of integers with `orderId` or `stopOrderId`.
3388        :param allOrdersIDs: pre-received lists of all active pending limit orders.
3389                             This avoids unnecessary downloading data from the server.
3390        :param allStopOrdersIDs: pre-received lists of all active stop orders.
3391        """
3392        if self.accountId is None or not self.accountId:
3393            uLogger.error("Variable `accountId` must be defined for using this method!")
3394            raise Exception("Account ID required")
3395
3396        if orderIDs:
3397            if allOrdersIDs is None:
3398                rawOrders = self.RequestPendingOrders()
3399                allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending limit orders ID
3400
3401            if allStopOrdersIDs is None:
3402                rawStopOrders = self.RequestStopOrders()
3403                allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3404
3405            for orderID in orderIDs:
3406                idInPendingOrders = orderID in allOrdersIDs
3407                idInStopOrders = orderID in allStopOrdersIDs
3408
3409                if not (idInPendingOrders or idInStopOrders):
3410                    uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID))
3411                    continue
3412
3413                else:
3414                    if idInPendingOrders:
3415                        uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID))
3416
3417                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder
3418                        self.body = str({"accountId": self.accountId, "orderId": orderID})
3419                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder"
3420                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3421
3422                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3423                            if self.moreDebug:
3424                                uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3425
3426                            uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID))
3427
3428                        else:
3429                            uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID))
3430
3431                    elif idInStopOrders:
3432                        uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID))
3433
3434                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder
3435                        self.body = str({"accountId": self.accountId, "stopOrderId": orderID})
3436                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder"
3437                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3438
3439                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3440                            if self.moreDebug:
3441                                uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3442
3443                            uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID))
3444
3445                        else:
3446                            uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID))
3447
3448                    else:
3449                        continue
3450
3451    def CloseAllOrders(self) -> None:
3452        """
3453        Gets a list of open pending and stop orders and cancel it all.
3454        """
3455        rawOrders = self.RequestPendingOrders()
3456        allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending limit orders ID
3457        lenOrders = len(allOrdersIDs)
3458
3459        rawStopOrders = self.RequestStopOrders()
3460        allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3461        lenSOrders = len(allStopOrdersIDs)
3462
3463        if lenOrders > 0 or lenSOrders > 0:
3464            uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders))
3465
3466            self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs)
3467
3468        else:
3469            uLogger.info("Orders not found, nothing to cancel.")
3470
3471    def CloseAll(self, *args) -> None:
3472        """
3473        Close all available (not blocked) opened trades and orders.
3474
3475        Also, you can select one or more keywords case-insensitive:
3476        `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type.
3477
3478        Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods.
3479        """
3480        overview = self.Overview(show=False)  # get all open trades info
3481
3482        if len(args) == 0:
3483            uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...")
3484            self.CloseAllOrders()  # close all pending and stop orders
3485
3486            for iType in TKS_INSTRUMENTS:
3487                if iType != "Currencies":
3488                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies
3489
3490        else:
3491            uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args)))
3492            lowerArgs = [x.lower() for x in args]
3493
3494            if "orders" in lowerArgs:
3495                self.CloseAllOrders()  # close all pending and stop orders
3496
3497            for iType in TKS_INSTRUMENTS:
3498                if iType.lower() in lowerArgs and iType != "Currencies":
3499                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies
3500
3501    def CloseAllByTicker(self, instrument: str) -> None:
3502        """
3503        Close all available (not blocked) opened trades and orders for one instrument defined by its ticker.
3504
3505        This method searches opened trade and orders of instrument throw all portfolio and then use
3506        `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument.
3507
3508        See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`.
3509
3510        :param instrument: string with ticker.
3511        """
3512        if instrument is None or not instrument:
3513            uLogger.error("Ticker name must be defined for using this method!")
3514            raise Exception("Ticker required")
3515
3516        overview = self.Overview(show=False)  # get user portfolio with all open trades info
3517
3518        self._ticker = instrument  # try to set instrument as ticker
3519        self._figi = ""
3520
3521        limitAll = [item["orderID"] for item in overview["stat"]["orders"]]  # list of all pending limit order IDs
3522        stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]]  # list of all stop order IDs
3523
3524        if limitAll and self.IsInLimitOrders(portfolio=overview):
3525            uLogger.debug("Closing all opened pending limit orders for the instrument with ticker [{}]. Wait, please...")
3526            self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3527
3528        if stopAll and self.IsInStopOrders(portfolio=overview):
3529            uLogger.debug("Closing all opened stop orders for the instrument with ticker [{}]. Wait, please...")
3530            self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3531
3532        if self.IsInPortfolio(portfolio=overview):
3533            uLogger.debug("Closing all available (not blocked) opened trade for the instrument with ticker [{}]. Wait, please...")
3534            self.CloseTrades(instruments=[instrument], portfolio=overview)
3535
3536    def CloseAllByFIGI(self, instrument: str) -> None:
3537        """
3538        Close all available (not blocked) opened trades and orders for one instrument defined by its FIGI id.
3539
3540        This method searches opened trade and orders of instrument throw all portfolio and then use
3541        `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument.
3542
3543        See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`.
3544
3545        :param instrument: string with FIGI id.
3546        """
3547        if instrument is None or not instrument:
3548            uLogger.error("FIGI id must be defined for using this method!")
3549            raise Exception("FIGI required")
3550
3551        overview = self.Overview(show=False)  # get user portfolio with all open trades info
3552
3553        self._ticker = ""
3554        self._figi = instrument  # try to set instrument as FIGI id
3555
3556        limitAll = [item["orderID"] for item in overview["stat"]["orders"]]  # list of all pending limit order IDs
3557        stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]]  # list of all stop order IDs
3558
3559        if limitAll and self.IsInLimitOrders(portfolio=overview):
3560            uLogger.debug("Closing all opened pending limit orders for the instrument with FIGI [{}]. Wait, please...")
3561            self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3562
3563        if stopAll and self.IsInStopOrders(portfolio=overview):
3564            uLogger.debug("Closing all opened stop orders for the instrument with FIGI [{}]. Wait, please...")
3565            self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3566
3567        if self.IsInPortfolio(portfolio=overview):
3568            uLogger.debug("Closing all available (not blocked) opened trade for the instrument with FIGI [{}]. Wait, please...")
3569            self.CloseTrades(instruments=[instrument], portfolio=overview)
3570
3571    @staticmethod
3572    def ParseOrderParameters(operation, **inputParameters):
3573        """
3574        Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.
3575
3576        :param operation: string "Buy" or "Sell".
3577        :param inputParameters: this is dict of strings that looks like this
3578               `{"lots": "L_int,...", "prices": "P_float,..."}` where
3579               "lots" key: one or more lot values (integer numbers) to open with every limit-order
3580               "prices" key: one or more prices to open limit-orders
3581               Counts of values in lots and prices lists must be equals!
3582        :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]`
3583        """
3584        # TODO: update order grid work with api v2
3585        pass
3586        # uLogger.debug("Input parameters: {}".format(inputParameters))
3587        #
3588        # if operation is None or not operation or operation not in ("Buy", "Sell"):
3589        #     uLogger.error("You must define operation type: 'Buy' or 'Sell'!")
3590        #     raise Exception("Incorrect value")
3591        #
3592        # if "l" in inputParameters.keys():
3593        #     inputParameters["lots"] = inputParameters.pop("l")
3594        #
3595        # if "p" in inputParameters.keys():
3596        #     inputParameters["prices"] = inputParameters.pop("p")
3597        #
3598        # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys():
3599        #     uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!")
3600        #     raise Exception("Incorrect value")
3601        #
3602        # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")]
3603        # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")]
3604        #
3605        # if len(lots) != len(prices):
3606        #     uLogger.error("'lots' and 'prices' lists must have equal length of values!")
3607        #     raise Exception("Incorrect value")
3608        #
3609        # uLogger.debug("Extracted parameters for orders:")
3610        # uLogger.debug("lots = {}".format(lots))
3611        # uLogger.debug("prices = {}".format(prices))
3612        #
3613        # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...]
3614        # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))]
3615        # uLogger.debug("Order parameters: {}".format(result))
3616        #
3617        # return result
3618
3619    def IsInPortfolio(self, portfolio: dict = None) -> bool:
3620        """
3621        Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`.
3622
3623        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3624        :return: `True` if portfolio contains open position with given instrument, `False` otherwise.
3625        """
3626        result = False
3627        msg = "Instrument not defined!"
3628
3629        if portfolio is None or not portfolio:
3630            portfolio = self.Overview(show=False)
3631
3632        if self._ticker:
3633            uLogger.debug("Searching instrument with ticker [{}] throw opened positions list...".format(self._ticker))
3634            msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker)
3635
3636            for iType in TKS_INSTRUMENTS:
3637                for instrument in portfolio["stat"][iType]:
3638                    if instrument["ticker"] == self._ticker:
3639                        result = True
3640                        msg = "Instrument with ticker [{}] is present in open positions".format(self._ticker)
3641                        break
3642
3643        elif self._figi:
3644            uLogger.debug("Searching instrument with FIGI [{}] throw opened positions list...".format(self._figi))
3645            msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi)
3646
3647            for iType in TKS_INSTRUMENTS:
3648                for instrument in portfolio["stat"][iType]:
3649                    if instrument["figi"] == self._figi:
3650                        result = True
3651                        msg = "Instrument with FIGI [{}] is present in open positions".format(self._figi)
3652                        break
3653
3654        else:
3655            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3656
3657        uLogger.debug(msg)
3658
3659        return result
3660
3661    def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict:
3662        """
3663        Returns instrument from the user's portfolio if it presents there.
3664        Instrument must be defined by `ticker` (highly priority) or `figi`.
3665
3666        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3667        :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise.
3668        """
3669        result = None
3670        msg = "Instrument not defined!"
3671
3672        if portfolio is None or not portfolio:
3673            portfolio = self.Overview(show=False)
3674
3675        if self._ticker:
3676            uLogger.debug("Searching instrument with ticker [{}] in opened positions...".format(self._ticker))
3677            msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker)
3678
3679            for iType in TKS_INSTRUMENTS:
3680                for instrument in portfolio["stat"][iType]:
3681                    if instrument["ticker"] == self._ticker:
3682                        result = instrument
3683                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self._ticker, instrument["figi"])
3684                        break
3685
3686        elif self._figi:
3687            uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self._figi))
3688            msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi)
3689
3690            for iType in TKS_INSTRUMENTS:
3691                for instrument in portfolio["stat"][iType]:
3692                    if instrument["figi"] == self._figi:
3693                        result = instrument
3694                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self._figi)
3695                        break
3696
3697        else:
3698            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3699
3700        uLogger.debug(msg)
3701
3702        return result
3703
3704    def IsInLimitOrders(self, portfolio: dict = None) -> bool:
3705        """
3706        Checks if instrument is in the limit orders list. Instrument must be defined by `ticker` (highly priority) or `figi`.
3707
3708        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3709
3710        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3711        :return: `True` if limit orders list contains some limit orders for the instrument, `False` otherwise.
3712        """
3713        result = False
3714        msg = "Instrument not defined!"
3715
3716        if portfolio is None or not portfolio:
3717            portfolio = self.Overview(show=False)
3718
3719        if self._ticker:
3720            uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker))
3721            msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker)
3722
3723            for instrument in portfolio["stat"]["orders"]:
3724                if instrument["ticker"] == self._ticker:
3725                    result = True
3726                    msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker)
3727                    break
3728
3729        elif self._figi:
3730            uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi))
3731            msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi)
3732
3733            for instrument in portfolio["stat"]["orders"]:
3734                if instrument["figi"] == self._figi:
3735                    result = True
3736                    msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi)
3737                    break
3738
3739        else:
3740            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3741
3742        uLogger.debug(msg)
3743
3744        return result
3745
3746    def GetLimitOrderIDs(self, portfolio: dict = None) -> list[str]:
3747        """
3748        Returns list with all `orderID`s of opened pending limit orders for the instrument.
3749        Instrument must be defined by `ticker` (highly priority) or `figi`.
3750
3751        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3752
3753        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3754        :return: list with `orderID`s of limit orders.
3755        """
3756        result = []
3757        msg = "Instrument not defined!"
3758
3759        if portfolio is None or not portfolio:
3760            portfolio = self.Overview(show=False)
3761
3762        if self._ticker:
3763            uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker))
3764            msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker)
3765
3766            for instrument in portfolio["stat"]["orders"]:
3767                if instrument["ticker"] == self._ticker:
3768                    result.append(instrument["orderID"])
3769
3770            if result:
3771                msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker)
3772
3773        elif self._figi:
3774            uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi))
3775            msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi)
3776
3777            for instrument in portfolio["stat"]["orders"]:
3778                if instrument["figi"] == self._figi:
3779                    result.append(instrument["orderID"])
3780
3781            if result:
3782                msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi)
3783
3784        else:
3785            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3786
3787        uLogger.debug(msg)
3788
3789        return result
3790
3791    def IsInStopOrders(self, portfolio: dict = None) -> bool:
3792        """
3793        Checks if instrument is in the stop orders list. Instrument must be defined by `ticker` (highly priority) or `figi`.
3794
3795        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3796
3797        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3798        :return: `True` if stop orders list contains some stop orders for the instrument, `False` otherwise.
3799        """
3800        result = False
3801        msg = "Instrument not defined!"
3802
3803        if portfolio is None or not portfolio:
3804            portfolio = self.Overview(show=False)
3805
3806        if self._ticker:
3807            uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker))
3808            msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker)
3809
3810            for instrument in portfolio["stat"]["stopOrders"]:
3811                if instrument["ticker"] == self._ticker:
3812                    result = True
3813                    msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker)
3814                    break
3815
3816        elif self._figi:
3817            uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi))
3818            msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi)
3819
3820            for instrument in portfolio["stat"]["stopOrders"]:
3821                if instrument["figi"] == self._figi:
3822                    result = True
3823                    msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi)
3824                    break
3825
3826        else:
3827            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3828
3829        uLogger.debug(msg)
3830
3831        return result
3832
3833    def GetStopOrderIDs(self, portfolio: dict = None) -> list[str]:
3834        """
3835        Returns list with all `orderID`s of opened stop orders for the instrument.
3836        Instrument must be defined by `ticker` (highly priority) or `figi`.
3837
3838        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3839
3840        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3841        :return: list with `orderID`s of stop orders.
3842        """
3843        result = []
3844        msg = "Instrument not defined!"
3845
3846        if portfolio is None or not portfolio:
3847            portfolio = self.Overview(show=False)
3848
3849        if self._ticker:
3850            uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker))
3851            msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker)
3852
3853            for instrument in portfolio["stat"]["stopOrders"]:
3854                if instrument["ticker"] == self._ticker:
3855                    result.append(instrument["orderID"])
3856
3857            if result:
3858                msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker)
3859
3860        elif self._figi:
3861            uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi))
3862            msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi)
3863
3864            for instrument in portfolio["stat"]["stopOrders"]:
3865                if instrument["figi"] == self._figi:
3866                    result.append(instrument["orderID"])
3867
3868            if result:
3869                msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi)
3870
3871        else:
3872            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3873
3874        uLogger.debug(msg)
3875
3876        return result
3877
3878    def RequestLimits(self) -> dict:
3879        """
3880        Method for obtaining the available funds for withdrawal for current `accountId`.
3881
3882        See also:
3883        - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits
3884        - `OverviewLimits()` method
3885
3886        :return: dict with raw data from server that contains free funds for withdrawal. Example of dict:
3887                 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`.
3888                 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency
3889                 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures.
3890        """
3891        if self.accountId is None or not self.accountId:
3892            uLogger.error("Variable `accountId` must be defined for using this method!")
3893            raise Exception("Account ID required")
3894
3895        uLogger.debug("Requesting current available funds for withdrawal. Wait, please...")
3896
3897        self.body = str({"accountId": self.accountId})
3898        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits"
3899        rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
3900
3901        if self.moreDebug:
3902            uLogger.debug("Records about available funds for withdrawal successfully received")
3903
3904        return rawLimits
3905
3906    def OverviewLimits(self, show: bool = False) -> dict:
3907        """
3908        Method for parsing and show table with available funds for withdrawal for current `accountId`.
3909
3910        See also: `RequestLimits()`.
3911
3912        :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log.
3913        :return: dict with raw parsed data from server and some calculated statistics about it.
3914        """
3915        if self.accountId is None or not self.accountId:
3916            uLogger.error("Variable `accountId` must be defined for using this method!")
3917            raise Exception("Account ID required")
3918
3919        rawLimits = self.RequestLimits()  # raw response with current available funds for withdrawal
3920
3921        view = {
3922            "rawLimits": rawLimits,
3923            "limits": {  # parsed data for every currency:
3924                "money": {  # this is an array of portfolio currency positions
3925                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"]
3926                },
3927                "blocked": {  # this is an array of blocked currency
3928                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"]
3929                },
3930                "blockedGuarantee": {  # this is locked money under collateral for futures
3931                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"]
3932                },
3933            },
3934        }
3935
3936        # --- Prepare text table with limits in human-readable format:
3937        if show:
3938            info = [
3939                "# Withdrawal limits\n\n",
3940                "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
3941                "* **Account ID:** [{}]\n".format(self.accountId),
3942            ]
3943
3944            if view["limits"]["money"]:
3945                info.extend([
3946                    "\n| Currencies | Total         | Available for withdrawal | Blocked for trade | Futures guarantee |\n",
3947                    "|------------|---------------|--------------------------|-------------------|-------------------|\n",
3948                ])
3949
3950            else:
3951                info.append("\nNo withdrawal limits\n")
3952
3953            for curr in view["limits"]["money"].keys():
3954                blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0
3955                blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0
3956                availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee)
3957
3958                infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format(
3959                    "[{}]".format(curr),
3960                    "{:.2f}".format(view["limits"]["money"][curr]),
3961                    "{:.2f}".format(availableMoney),
3962                    "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—",
3963                    "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—",
3964                )
3965
3966                if curr == "rub":
3967                    info.insert(5, infoStr)  # hack: insert "rub" at the first position in table and after headers
3968
3969                else:
3970                    info.append(infoStr)
3971
3972            infoText = "".join(info)
3973
3974            uLogger.info(infoText)
3975
3976            if self.withdrawalLimitsFile:
3977                with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH:
3978                    fH.write(infoText)
3979
3980                uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile)))
3981
3982                if self.useHTMLReports:
3983                    htmlFilePath = self.withdrawalLimitsFile.replace(".md", ".html") if self.withdrawalLimitsFile.endswith(".md") else self.withdrawalLimitsFile + ".html"
3984                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
3985                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Withdrawal limits", commonCSS=COMMON_CSS, markdown=infoText))
3986
3987                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
3988
3989        return view
3990
3991    def RequestAccounts(self) -> dict:
3992        """
3993        Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`.
3994
3995        See also:
3996        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts
3997        - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account
3998        - `OverviewUserInfo()` method
3999
4000        :return: dict with raw data from server that contains accounts info. Example of dict:
4001                 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account",
4002                   "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z",
4003                   "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`.
4004                 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now.
4005        """
4006        uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...")
4007
4008        self.body = str({})
4009        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts"
4010        rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST")
4011
4012        if self.moreDebug:
4013            uLogger.debug("Records about available accounts successfully received")
4014
4015        return rawAccounts
4016
4017    def RequestUserInfo(self) -> dict:
4018        """
4019        Method for requesting common user's information.
4020
4021        See also:
4022        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo
4023        - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest
4024        - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with
4025        - `OverviewUserInfo()` method
4026
4027        :return: dict with raw data from server that contains user's information. Example of dict:
4028                 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage",
4029                   "russian_shares", "structured_income_bonds"], "tariff": "premium"}`.
4030        """
4031        uLogger.debug("Requesting common user's information. Wait, please...")
4032
4033        self.body = str({})
4034        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo"
4035        rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST")
4036
4037        if self.moreDebug:
4038            uLogger.debug("Records about current user successfully received")
4039
4040        return rawUserInfo
4041
4042    def RequestMarginStatus(self, accountId: str = None) -> dict:
4043        """
4044        Method for requesting margin calculation for defined account ID.
4045
4046        See also:
4047        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes
4048        - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse
4049        - `OverviewUserInfo()` method
4050
4051        :param accountId: string with numeric account ID. If `None`, then used class field `accountId`.
4052        :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict.
4053                 Example of responses:
4054                 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`.
4055                 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000},
4056                                    "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000},
4057                                    "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000},
4058                                    "fundsSufficiencyLevel": {"units": "1", "nano": 280000000},
4059                                    "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`.
4060        """
4061        if accountId is None or not accountId:
4062            if self.accountId is None or not self.accountId:
4063                uLogger.error("Variable `accountId` must be defined for using this method!")
4064                raise Exception("Account ID required")
4065
4066            else:
4067                accountId = self.accountId  # use `self.accountId` (main ID) by default
4068
4069        uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId))
4070
4071        self.body = str({"accountId": accountId})
4072        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes"
4073        rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST")
4074
4075        if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}:
4076            uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId))
4077            rawMargin = {}
4078
4079        else:
4080            if self.moreDebug:
4081                uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId))
4082
4083        return rawMargin
4084
4085    def RequestTariffLimits(self) -> dict:
4086        """
4087        Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`.
4088
4089        See also:
4090        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff
4091        - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest
4092        - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit
4093        - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit
4094        - `OverviewUserInfo()` method
4095
4096        :return: dict with raw data from server that contains limits of current tariff. Example of dict:
4097                 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...],
4098                   "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`.
4099        """
4100        uLogger.debug("Requesting limits of current tariff. Wait, please...")
4101
4102        self.body = str({})
4103        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff"
4104        rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
4105
4106        if self.moreDebug:
4107            uLogger.debug("Records with limits of current tariff successfully received")
4108
4109        return rawTariffLimits
4110
4111    def RequestBondCoupons(self, iJSON: dict) -> dict:
4112        """
4113        Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown
4114        then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`.
4115        All dates are in UTC timezone.
4116
4117        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons
4118        Documentation:
4119        - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest
4120        - response: https://tinkoff.github.io/investAPI/instruments/#coupon
4121
4122        See also: `ExtendBondsData()`.
4123
4124        :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self._ticker]`
4125                      If raw iJSON is not data of bond then server returns an error [400] with message:
4126                      `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`.
4127        :return: dictionary with bond payment calendar. Response example
4128                 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12",
4129                   "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000},
4130                   "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z",
4131                   "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}`
4132        """
4133        if iJSON["figi"] is None or not iJSON["figi"]:
4134            uLogger.error("FIGI must be defined for using this method!")
4135            raise Exception("FIGI required")
4136
4137        startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z"
4138        endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z"
4139
4140        uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format(
4141            "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "",
4142            self._figi,
4143            startDate,
4144            endDate,
4145        ))
4146
4147        self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate})
4148        calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons"
4149        calendar = self.SendAPIRequest(calendarURL, reqType="POST")
4150
4151        if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}:
4152            uLogger.warning("Instrument type is not bond!")
4153
4154        else:
4155            if self.moreDebug:
4156                uLogger.debug("Records about bond payment calendar successfully received")
4157
4158        return calendar
4159
4160    def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame:
4161        """
4162        Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider
4163        Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar,
4164        coupon yields, current yields and some statistics etc.
4165
4166        WARNING! This is too long operation if a lot of bonds requested from broker server.
4167
4168        See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`.
4169
4170        :param instruments: list of strings with tickers or FIGIs.
4171        :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`,
4172                     for further used by data scientists or stock analytics.
4173        :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker.
4174                 In XLSX-file and Pandas DataFrame fields mean:
4175                 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond
4176                 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon
4177        """
4178        if instruments is None or not instruments:
4179            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
4180            raise Exception("Ticker or FIGI required")
4181
4182        if isinstance(instruments, str):
4183            instruments = [instruments]
4184
4185        uniqueInstruments = self.GetUniqueFIGIs(instruments)
4186
4187        uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...")
4188
4189        iCount = len(uniqueInstruments)
4190        tooLong = iCount >= 20
4191        if tooLong:
4192            uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...")
4193
4194        bonds = None
4195        for i, self._figi in enumerate(uniqueInstruments):
4196            instrument = self.SearchByFIGI(requestPrice=False)  # raw data about instrument from server
4197
4198            if "type" in instrument.keys() and instrument["type"] == "Bonds":
4199                # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond
4200                rawBond = self.SearchByFIGI(requestPrice=True)
4201
4202                # Widen raw data with UTC current time (iData["actualDateTime"]):
4203                actualDate = datetime.now(tzutc())
4204                iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond
4205
4206                # Widen raw data with bond payment calendar (iData["rawCalendar"]):
4207                iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)}
4208
4209                # Replace some values with human-readable:
4210                iData["nominalCurrency"] = iData["nominal"]["currency"]
4211                iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"])
4212                iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"])
4213                iData["aciCurrency"] = iData["aciValue"]["currency"]
4214                iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"])
4215                iData["issueSize"] = int(iData["issueSize"])
4216                iData["issueSizePlan"] = int(iData["issueSizePlan"])
4217                iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]]
4218                iData["step"] = iData["step"] if "step" in iData.keys() else 0
4219                iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]]
4220                iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0
4221                iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0
4222                iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0
4223                iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0
4224                iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0
4225                iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0
4226
4227                # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date):
4228                iData["limitUpPercent"] = iData["currentPrice"]["limitUp"]  # max price on current day in percents of nominal
4229                iData["limitDownPercent"] = iData["currentPrice"]["limitDown"]  # min price on current day in percents of nominal
4230                iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"]  # last price on market in percents of nominal
4231                iData["closePricePercent"] = iData["currentPrice"]["closePrice"]  # previous day close in percents of nominal
4232                iData["changes"] = iData["currentPrice"]["changes"]  # this is percent of changes between `currentPrice` and `lastPrice`
4233                iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100  # max price on current day is `limitUpPercent` * `nominal`
4234                iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100  # min price on current day is `limitDownPercent` * `nominal`
4235                iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100  # last price on market is `lastPricePercent` * `nominal`
4236                iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100  # previous day close is `closePricePercent` * `nominal`
4237                iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"]  # this is delta between last deal price and last close
4238
4239                # Widen raw data with calendar data from `rawCalendar` values:
4240                calendarData = []
4241                if "events" in iData["rawCalendar"].keys():
4242                    for item in iData["rawCalendar"]["events"]:
4243                        calendarData.append({
4244                            "couponDate": item["couponDate"],
4245                            "couponNumber": int(item["couponNumber"]),
4246                            "fixDate": item["fixDate"] if "fixDate" in item.keys() else "",
4247                            "payCurrency": item["payOneBond"]["currency"],
4248                            "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]),
4249                            "couponType": TKS_COUPON_TYPES[item["couponType"]],
4250                            "couponStartDate": item["couponStartDate"],
4251                            "couponEndDate": item["couponEndDate"],
4252                            "couponPeriod": item["couponPeriod"],
4253                        })
4254
4255                    # if maturity date is unknown then uses the latest date in bond payment calendar for it:
4256                    if "maturityDate" not in iData.keys():
4257                        iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else ""
4258
4259                # Widen raw data with Coupon Rate.
4260                # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%:
4261                iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData])
4262                iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData])
4263                iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0.
4264
4265                # Widen raw data with Yield to Maturity (YTM) on current date.
4266                # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%:
4267                maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None
4268                iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None
4269                iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate])
4270                iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"]  # sum of all last coupons minus current ACI value
4271                iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0.
4272
4273                iData["calendar"] = calendarData  # adds calendar at the end
4274
4275                # Remove not used data:
4276                iData.pop("uid")
4277                iData.pop("positionUid")
4278                iData.pop("currentPrice")
4279                iData.pop("rawCalendar")
4280
4281                colNames = list(iData.keys())
4282                if bonds is None:
4283                    bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames))
4284
4285                else:
4286                    bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True)
4287
4288            else:
4289                uLogger.warning("Instrument is not a bond!")
4290
4291            processed = round(100 * (i + 1) / iCount, 1)
4292            if tooLong and processed % 5 == 0:
4293                uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount))
4294
4295            else:
4296                uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount))
4297
4298        bonds.index = bonds["ticker"].tolist()  # replace indexes with ticker names
4299
4300        # Saving bonds from Pandas DataFrame to XLSX sheet:
4301        if xlsx and self.bondsXLSXFile:
4302            with pd.ExcelWriter(
4303                    path=self.bondsXLSXFile,
4304                    date_format=TKS_DATE_FORMAT,
4305                    datetime_format=TKS_DATE_TIME_FORMAT,
4306                    mode="w",
4307            ) as writer:
4308                bonds.to_excel(
4309                    writer,
4310                    sheet_name="Extended bonds data",
4311                    index=True,
4312                    encoding="UTF-8",
4313                    freeze_panes=(1, 1),
4314                )  # saving as XLSX-file with freeze first row and column as headers
4315
4316            uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile)))
4317
4318        return bonds
4319
4320    def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame:
4321        """
4322        Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default.
4323
4324        WARNING! This is too long operation if a lot of bonds requested from broker server.
4325
4326        See also: `ShowBondsCalendar()`, `ExtendBondsData()`.
4327
4328        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
4329                        extended information about bonds: main info, current prices, bond payment calendar,
4330                        coupon yields, current yields and some statistics etc.
4331                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4332        :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default,
4333                     for further used by data scientists or stock analytics.
4334        :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon
4335        """
4336        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4337            extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=False)
4338
4339        uLogger.debug("Generating bond payments calendar data. Wait, please...")
4340
4341        colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"]
4342        colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"]
4343        calendar = None
4344        for bond in extBonds.iterrows():
4345            for item in bond[1]["calendar"]:
4346                cData = {
4347                    "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()),
4348                    "couponDate": item["couponDate"],
4349                    "figi": bond[1]["figi"],
4350                    "ticker": bond[1]["ticker"],
4351                    "name": bond[1]["name"],
4352                    "couponNumber": item["couponNumber"],
4353                    "payOneBond": item["payOneBond"],
4354                    "payCurrency": item["payCurrency"],
4355                    "couponType": item["couponType"],
4356                    "couponPeriod": item["couponPeriod"],
4357                    "fixDate": item["fixDate"],
4358                    "couponStartDate": item["couponStartDate"],
4359                    "couponEndDate": item["couponEndDate"],
4360                }
4361
4362                if calendar is None:
4363                    calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID))
4364
4365                else:
4366                    calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True)
4367
4368        if calendar is not None:
4369            calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True)  # sort all payments for all bonds by payment date
4370
4371            # Saving calendar from Pandas DataFrame to XLSX sheet:
4372            if xlsx:
4373                xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx"
4374
4375                with pd.ExcelWriter(
4376                        path=xlsxCalendarFile,
4377                        date_format=TKS_DATE_FORMAT,
4378                        datetime_format=TKS_DATE_TIME_FORMAT,
4379                        mode="w",
4380                ) as writer:
4381                    humanReadable = calendar.copy(deep=True)
4382                    humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0])
4383                    humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0])
4384                    humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0])
4385                    humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0])
4386                    humanReadable.columns = colNames  # human-readable column names
4387
4388                    humanReadable.to_excel(
4389                        writer,
4390                        sheet_name="Bond payments calendar",
4391                        index=False,
4392                        encoding="UTF-8",
4393                        freeze_panes=(1, 2),
4394                    )  # saving as XLSX-file with freeze first row and column as headers
4395
4396                    del humanReadable  # release df in memory
4397
4398                uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile)))
4399
4400        return calendar
4401
4402    def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True) -> str:
4403        """
4404        Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond.
4405        Also, creates Markdown file with calendar data, `calendar.md` by default.
4406
4407        See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`.
4408
4409        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
4410                        extended information about bonds: main info, current prices, bond payment calendar,
4411                        coupon yields, current yields and some statistics etc.
4412                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4413        :param show: if `True` then also printing bonds payment calendar to the console,
4414                     otherwise save to file `calendarFile` only. `False` by default.
4415        :return: multilines text in Markdown format with bonds payment calendar as a table.
4416        """
4417        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4418            extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=False)
4419
4420        infoText = "# Bond payments calendar\n\n"
4421
4422        calendar = self.CreateBondsCalendar(extBonds, xlsx=True)  # generate Pandas DataFrame with full calendar data
4423
4424        if not (calendar is None or calendar.empty):
4425            splitLine = "|       |                 |              |              |     |               |           |        |                   |\n"
4426
4427            info = [
4428                "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4429                "| Paid  | Payment date    | FIGI         | Ticker       | No. | Value         | Type      | Period | End registry date |\n",
4430                "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n",
4431            ]
4432
4433            newMonth = False
4434            notOneBond = calendar["figi"].nunique() > 1
4435            for i, bond in enumerate(calendar.iterrows()):
4436                if newMonth and notOneBond:
4437                    info.append(splitLine)
4438
4439                info.append(
4440                    "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format(
4441                        "  √" if bond[1]["paid"] else "  —",
4442                        bond[1]["couponDate"].split("T")[0],
4443                        bond[1]["figi"],
4444                        bond[1]["ticker"],
4445                        bond[1]["couponNumber"],
4446                        "{} {}".format(
4447                            "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."),
4448                            bond[1]["payCurrency"],
4449                        ),
4450                        bond[1]["couponType"],
4451                        bond[1]["couponPeriod"],
4452                        bond[1]["fixDate"].split("T")[0],
4453                    )
4454                )
4455
4456                if i < len(calendar.values) - 1:
4457                    curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4458                    nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4459                    newMonth = False if curDate.month == nextDate.month else True
4460
4461                else:
4462                    newMonth = False
4463
4464            infoText += "".join(info)
4465
4466            if show:
4467                uLogger.info("{}".format(infoText))
4468
4469            if self.calendarFile is not None:
4470                with open(self.calendarFile, "w", encoding="UTF-8") as fH:
4471                    fH.write(infoText)
4472
4473                uLogger.info("Bond payments calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile)))
4474
4475                if self.useHTMLReports:
4476                    htmlFilePath = self.calendarFile.replace(".md", ".html") if self.calendarFile.endswith(".md") else self.calendarFile + ".html"
4477                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
4478                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Bond payments calendar", commonCSS=COMMON_CSS, markdown=infoText))
4479
4480                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
4481
4482        else:
4483            infoText += "No data\n"
4484
4485        return infoText
4486
4487    def OverviewAccounts(self, show: bool = False) -> dict:
4488        """
4489        Method for parsing and show simple table with all available user accounts.
4490
4491        See also: `RequestAccounts()` and `OverviewUserInfo()` methods.
4492
4493        :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log.
4494        :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict:
4495                 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...},
4496                          "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1",
4497                                                        "status": "Opened and active account", "opened": "2018-05-23 00:00:00",
4498                                                        "closed": "—", "access": "Full access" }, ...}}`
4499        """
4500        rawAccounts = self.RequestAccounts()  # Raw responses with accounts
4501
4502        # This is an array of dict with user accounts, its `accountId`s and some parsed data:
4503        accounts = {
4504            item["id"]: {
4505                "type": TKS_ACCOUNT_TYPES[item["type"]],
4506                "name": item["name"],
4507                "status": TKS_ACCOUNT_STATUSES[item["status"]],
4508                "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4509                "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—",
4510                "access": TKS_ACCESS_LEVELS[item["accessLevel"]],
4511            } for item in rawAccounts["accounts"]
4512        }
4513
4514        # Raw and parsed data with some fields replaced in "stat" section:
4515        view = {
4516            "rawAccounts": rawAccounts,
4517            "stat": accounts,
4518        }
4519
4520        # --- Prepare simple text table with only accounts data in human-readable format:
4521        if show:
4522            info = [
4523                "# User accounts\n\n",
4524                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4525                "| Account ID   | Type                      | Status                    | Name                           |\n",
4526                "|--------------|---------------------------|---------------------------|--------------------------------|\n",
4527            ]
4528
4529            for account in view["stat"].keys():
4530                info.extend([
4531                    "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format(
4532                        account,
4533                        view["stat"][account]["type"],
4534                        view["stat"][account]["status"],
4535                        view["stat"][account]["name"],
4536                    )
4537                ])
4538
4539            infoText = "".join(info)
4540
4541            uLogger.info(infoText)
4542
4543            if self.userAccountsFile:
4544                with open(self.userAccountsFile, "w", encoding="UTF-8") as fH:
4545                    fH.write(infoText)
4546
4547                uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile)))
4548
4549                if self.useHTMLReports:
4550                    htmlFilePath = self.userAccountsFile.replace(".md", ".html") if self.userAccountsFile.endswith(".md") else self.userAccountsFile + ".html"
4551                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
4552                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User accounts", commonCSS=COMMON_CSS, markdown=infoText))
4553
4554                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
4555
4556        return view
4557
4558    def OverviewUserInfo(self, show: bool = False) -> dict:
4559        """
4560        Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit).
4561
4562        See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods.
4563
4564        :param show: if `False` then only dictionary returns, if `True` then also print user's data to log.
4565        :return: dict with raw parsed data from server and some calculated statistics about it.
4566        """
4567        overview = self.Overview(show=False)  # Request current user portfolio for the ability to calculate missing funds
4568        tmpTicker = self._ticker
4569        self._ticker = "RUB000UTSTOM"  # This instrument show in rub how much money cost current margin
4570        missing = self.GetInstrumentFromPortfolio(portfolio=overview)
4571        self._ticker = tmpTicker
4572
4573        rawUserInfo = self.RequestUserInfo()  # Raw response with common user info
4574        overviewAccount = self.OverviewAccounts(show=False)  # Raw and parsed accounts data
4575        rawAccounts = overviewAccount["rawAccounts"]  # Raw response with user accounts data
4576        accounts = overviewAccount["stat"]  # Dict with only statistics about user accounts
4577        rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()}  # Raw response with margin calculation for every account ID
4578        rawTariffLimits = self.RequestTariffLimits()  # Raw response with limits of current tariff
4579
4580        # This is dict with parsed common user data:
4581        userInfo = {
4582            "premium": "Yes" if rawUserInfo["premStatus"] else "No",
4583            "qualified": "Yes" if rawUserInfo["qualStatus"] else "No",
4584            "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]],
4585            "tariff": rawUserInfo["tariff"],
4586        }
4587
4588        # This is an array of dict with parsed margin statuses for every account IDs:
4589        margins = {}
4590        for accountId in accounts.keys():
4591            if rawMargins[accountId]:
4592                margins[accountId] = {
4593                    "currency": rawMargins[accountId]["liquidPortfolio"]["currency"],
4594                    "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]),
4595                    "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]),
4596                    "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]),
4597                    "diff": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]),
4598                    "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]),
4599                    "missing": missing["volume"],
4600                }
4601
4602            else:
4603                margins[accountId] = {}  # Server response: margin status is disabled for current accountId
4604
4605        unary = {}  # unary-connection limits
4606        for item in rawTariffLimits["unaryLimits"]:
4607            if item["limitPerMinute"] in unary.keys():
4608                unary[item["limitPerMinute"]].extend(item["methods"])
4609
4610            else:
4611                unary[item["limitPerMinute"]] = item["methods"]
4612
4613        stream = {}  # stream-connection limits
4614        for item in rawTariffLimits["streamLimits"]:
4615            if item["limit"] in stream.keys():
4616                stream[item["limit"]].extend(item["streams"])
4617
4618            else:
4619                stream[item["limit"]] = item["streams"]
4620
4621        # This is dict with parsed limits of current tariff (connections, API methods etc.):
4622        limits = {
4623            "unary": unary,
4624            "stream": stream,
4625        }
4626
4627        # Raw and parsed data as an output result:
4628        view = {
4629            "rawUserInfo": rawUserInfo,
4630            "rawAccounts": rawAccounts,
4631            "rawMargins": rawMargins,
4632            "rawTariffLimits": rawTariffLimits,
4633            "stat": {
4634                "overview": overview,
4635                "userInfo": userInfo,
4636                "accounts": accounts,
4637                "margins": margins,
4638                "limits": limits,
4639            },
4640        }
4641
4642        # --- Prepare text table with user information in human-readable format:
4643        if show:
4644            info = [
4645                "# Full user information\n\n",
4646                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4647                "## Common information\n\n",
4648                "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]),
4649                "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]),
4650                "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]),
4651                "* **Allowed to work with instruments:**\n{}\n".format("".join(["  - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])),
4652                "\n## User accounts\n\n",
4653            ]
4654
4655            for account in view["stat"]["accounts"].keys():
4656                info.extend([
4657                    "### ID: [{}]\n\n".format(account),
4658                    "| Parameters           | Values                                                       |\n",
4659                    "|----------------------|--------------------------------------------------------------|\n",
4660                    "| Account type:        | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]),
4661                    "| Account name:        | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]),
4662                    "| Account status:      | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]),
4663                    "| Access level:        | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]),
4664                    "| Date opened:         | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]),
4665                    "| Date closed:         | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]),
4666                ])
4667
4668                if margins[account]:
4669                    info.extend([
4670                        "| Margin status:       | Enabled                                                      |\n",
4671                        "| - Liquid portfolio:  | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])),
4672                        "| - Margin starting:   | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])),
4673                        "| - Margin minimum:    | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])),
4674                        "| - Margin difference: | {:<60} |\n".format("{} {}".format(margins[account]["diff"], margins[account]["currency"])),
4675                        "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)),
4676                        "| - Not covered funds: | {:<60} |\n\n".format("{:.2f} {}".format(margins[account]["missing"], margins[account]["currency"])),
4677                    ])
4678
4679                else:
4680                    info.append("| Margin status:       | Disabled                                                     |\n\n")
4681
4682            info.extend([
4683                "\n## Current user tariff limits\n",
4684                "\n### See also\n",
4685                "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n",
4686                "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n",
4687                "  - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n",
4688                "  - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n",
4689                "\n### Unary limits\n",
4690            ])
4691
4692            if unary:
4693                for key, values in sorted(unary.items()):
4694                    info.append("\n* Max requests per minute: {}\n".format(key))
4695
4696                    for value in values:
4697                        info.append("  - {}\n".format(value))
4698
4699            else:
4700                info.append("\nNot available\n")
4701
4702            info.append("\n### Stream limits\n")
4703
4704            if stream:
4705                for key, values in sorted(stream.items()):
4706                    info.append("\n* Max stream connections: {}\n".format(key))
4707
4708                    for value in values:
4709                        info.append("  - {}\n".format(value))
4710
4711            else:
4712                info.append("\nNot available\n")
4713
4714            infoText = "".join(info)
4715
4716            uLogger.info(infoText)
4717
4718            if self.userInfoFile:
4719                with open(self.userInfoFile, "w", encoding="UTF-8") as fH:
4720                    fH.write(infoText)
4721
4722                uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile)))
4723
4724                if self.useHTMLReports:
4725                    htmlFilePath = self.userInfoFile.replace(".md", ".html") if self.userInfoFile.endswith(".md") else self.userInfoFile + ".html"
4726                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
4727                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User info", commonCSS=COMMON_CSS, markdown=infoText))
4728
4729                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
4730
4731        return view
4732
4733
4734class Args:
4735    """
4736    If `Main()` function is imported as module, then this class used to convert arguments from **kwargs as object.
4737    """
4738    def __init__(self, **kwargs):
4739        self.__dict__.update(kwargs)
4740
4741    def __getattr__(self, item):
4742        return None
4743
4744
4745def ParseArgs():
4746    """This function get and parse command line keys."""
4747    parser = ArgumentParser()  # command-line string parser
4748
4749    parser.description = "TKSBrokerAPI is a trading platform for automation on Python to simplify the implementation of trading scenarios and work with Tinkoff Invest API server via the REST protocol. See examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md"
4750    parser.usage = "\n/as module/ python TKSBrokerAPI.py [some options] [one command]\n/as CLI tool/ tksbrokerapi [some options] [one command]"
4751
4752    # --- options:
4753
4754    parser.add_argument("--no-cache", action="store_true", default=False, help="Option: not use local cache `dump.json`, but update raw instruments data when starting the platform. `False` by default.")
4755    parser.add_argument("--token", type=str, help="Option: Tinkoff service's api key. If not set then used environment variable `TKS_API_TOKEN`. See how to use: https://tinkoff.github.io/investAPI/token/")
4756    parser.add_argument("--account-id", type=str, default=None, help="Option: string with an user numeric account ID in Tinkoff Broker. It can be found in any broker's reports (see the contract number). Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.")
4757
4758    parser.add_argument("--ticker", "-t", type=str, help="Option: instrument's ticker, e.g. `IBM`, `YNDX`, `GOOGL` etc. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR`.")
4759    parser.add_argument("--figi", "-f", type=str, help="Option: instrument's FIGI, e.g. `BBG006L8G4H1` (for `YNDX`).")
4760
4761    parser.add_argument("--depth", type=int, default=1, help="Option: Depth of Market (DOM) can be >=1, 1 by default.")
4762    parser.add_argument("--no-cancelled", "--no-canceled", action="store_true", default=False, help="Option: remove information about cancelled operations from the deals report by the `--deals` key. `False` by default.")
4763
4764    parser.add_argument("--output", type=str, default=None, help="Option: replace default paths to output files for some commands. If `None` then used default files.")
4765    parser.add_argument("--html", "--HTML", action="store_true", default=False, help="Option: if key present then TKSBrokerAPI generate also HTML reports from Markdown. False by default.")
4766
4767    parser.add_argument("--interval", type=str, default="hour", help="Option: available values are `1min`, `5min`, `15min`, `hour` and `day`. Used only with `--history` key. This is time period of one candle. Default: `hour` for every history candles.")
4768    parser.add_argument("--only-missing", action="store_true", default=False, help="Option: if history file define by `--output` key then add only last missing candles, do not request all history length. `False` by default.")
4769    parser.add_argument("--csv-sep", type=str, default=",", help="Option: separator if csv-file is used, `,` by default.")
4770
4771    parser.add_argument("--debug-level", "--log-level", "--verbosity", "-v", type=int, default=20, help="Option: showing STDOUT messages of minimal debug level, e.g. 10 = DEBUG, 20 = INFO, 30 = WARNING, 40 = ERROR, 50 = CRITICAL. INFO (20) by default.")
4772    parser.add_argument("--more", "--more-debug", action="store_true", default=False, help="Option: `--debug-level` key only switch log level verbosity, but in addition `--more` key enable all debug information, such as net request and response headers in all methods.")
4773
4774    # --- commands:
4775
4776    parser.add_argument("--version", "--ver", action="store_true", help="Action: shows current semantic version, looks like `major.minor.buildnumber`. If TKSBrokerAPI not installed via pip, then used local build number `.dev0`.")
4777
4778    parser.add_argument("--list", "-l", action="store_true", help="Action: get and print all available instruments and some information from broker server. Also, you can define `--output` key to save list of instruments to file, default: `instruments.md`.")
4779    parser.add_argument("--list-xlsx", "-x", action="store_true", help="Action: get all available instruments from server for current account and save raw data into xlsx-file for further used by data scientists or stock analytics, default: `dump.xlsx`.")
4780    parser.add_argument("--bonds-xlsx", "-b", type=str, nargs="*", help="Action: get all available bonds if only key present or list of bonds with FIGIs or tickers and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bonds payment calendar, coupon yields, current yields and some statistics etc. And then export data to XLSX-file, default: `ext-bonds.xlsx` or you can change it with `--output` key. WARNING! This is too long operation if a lot of bonds requested from broker server.")
4781    parser.add_argument("--search", "-s", type=str, nargs=1, help="Action: search for an instruments by part of the name, ticker or FIGI. Also, you can define `--output` key to save results to file, default: `search-results.md`.")
4782    parser.add_argument("--info", "-i", action="store_true", help="Action: get information from broker server about instrument by it's ticker or FIGI. `--ticker` key or `--figi` key must be defined!")
4783    parser.add_argument("--calendar", "-c", type=str, nargs="*", help="Action: show bonds payment calendar as a table. Calendar build for one or more tickers or FIGIs, or for all bonds if only key present. If the `--output` key present then calendar saves to file, default: `calendar.md`. Also, created XLSX-file with bond payments calendar for further used by data scientists or stock analytics, `calendar.xlsx` by default. WARNING! This is too long operation if a lot of bonds requested from broker server.")
4784    parser.add_argument("--price", action="store_true", help="Action: show actual price list for current instrument. Also, you can use `--depth` key. `--ticker` key or `--figi` key must be defined!")
4785    parser.add_argument("--prices", "-p", type=str, nargs="+", help="Action: get and print current prices for list of given instruments (by it's tickers or by FIGIs). WARNING! This is too long operation if you request a lot of instruments! Also, you can define `--output` key to save list of prices to file, default: `prices.md`.")
4786
4787    parser.add_argument("--overview", "-o", action="store_true", help="Action: shows all open positions, orders and some statistics. Also, you can define `--output` key to save this information to file, default: `overview.md`.")
4788    parser.add_argument("--overview-digest", action="store_true", help="Action: shows a short digest of the portfolio status. Also, you can define `--output` key to save this information to file, default: `overview-digest.md`.")
4789    parser.add_argument("--overview-positions", action="store_true", help="Action: shows only open positions. Also, you can define `--output` key to save this information to file, default: `overview-positions.md`.")
4790    parser.add_argument("--overview-orders", action="store_true", help="Action: shows only sections of open limits and stop orders. Also, you can define `--output` key to save orders to file, default: `overview-orders.md`.")
4791    parser.add_argument("--overview-analytics", action="store_true", help="Action: shows only the analytics section and the distribution of the portfolio by various categories. Also, you can define `--output` key to save this information to file, default: `overview-analytics.md`.")
4792    parser.add_argument("--overview-calendar", action="store_true", help="Action: shows only the bonds calendar section (if these present in portfolio). Also, you can define `--output` key to save this information to file, default: `overview-calendar.md`.")
4793
4794    parser.add_argument("--deals", "-d", type=str, nargs="*", help="Action: show all deals between two given dates. Start day may be an integer number: -1, -2, -3 days ago. Also, you can use keywords: `today`, `yesterday` (-1), `week` (-7), `month` (-30) and `year` (-365). Dates format must be: `%%Y-%%m-%%d`, e.g. 2020-02-03. With `--no-cancelled` key information about cancelled operations will be removed from the deals report. Also, you can define `--output` key to save all deals to file, default: `deals.md`.")
4795    parser.add_argument("--history", type=str, nargs="*", help="Action: get last history candles of the current instrument defined by `--ticker` or `--figi` (FIGI id) keys. History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. This action may be used together with the `--render-chart` key. Also, you can define `--output` key to save history candlesticks to file.")
4796    parser.add_argument("--load-history", type=str, help="Action: try to load history candles from given csv-file as a Pandas Dataframe and print it in to the console. This action may be used together with the `--render-chart` key.")
4797    parser.add_argument("--render-chart", type=str, help="Action: render candlesticks chart. This key may only used with `--history` or `--load-history` together. Action has 1 parameter with two possible string values: `interact` (`i`) or `non-interact` (`ni`).")
4798
4799    parser.add_argument("--trade", nargs="*", help="Action: universal action to open market position for defined ticker or FIGI. You must specify 1-5 parameters: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. See examples in readme.")
4800    parser.add_argument("--buy", nargs="*", help="Action: immediately open BUY market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].")
4801    parser.add_argument("--sell", nargs="*", help="Action: immediately open SELL market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].")
4802
4803    parser.add_argument("--order", nargs="*", help="Action: universal action to open limit or stop-order in any directions. You must specify 4-7 parameters: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]]. See examples in readme.")
4804    parser.add_argument("--buy-limit", type=float, nargs=2, help="Action: open pending BUY limit-order (below current price). You must specify only 2 parameters: [lots] [target price] to open BUY limit-order. If you try to create `Buy` limit-order above current price then broker immediately open `Buy` market order, such as if you do simple `--buy` operation!")
4805    parser.add_argument("--sell-limit", type=float, nargs=2, help="Action: open pending SELL limit-order (above current price). You must specify only 2 parameters: [lots] [target price] to open SELL limit-order. If you try to create `Sell` limit-order below current price then broker immediately open `Sell` market order, such as if you do simple `--sell` operation!")
4806    parser.add_argument("--buy-stop", nargs="*", help="Action: open BUY stop-order. You must specify at least 2 parameters: [lots] [target price] to open BUY stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.")
4807    parser.add_argument("--sell-stop", nargs="*", help="Action: open SELL stop-order. You must specify at least 2 parameters: [lots] [target price] to open SELL stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.")
4808    # parser.add_argument("--buy-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending BUY limit-orders (below current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!")
4809    # parser.add_argument("--sell-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending SELL limit-orders (above current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!")
4810
4811    parser.add_argument("--close-order", "--cancel-order", type=str, nargs=1, help="Action: close only one order by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.")
4812    parser.add_argument("--close-orders", "--cancel-orders", type=str, nargs="+", help="Action: close one or list of orders by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.")
4813    parser.add_argument("--close-trade", "--cancel-trade", action="store_true", help="Action: close only one position for instrument defined by `--ticker` (high priority) or `--figi` keys, including for currencies tickers.")
4814    parser.add_argument("--close-trades", "--cancel-trades", type=str, nargs="+", help="Action: close positions for list of tickers or FIGIs, including for currencies tickers or FIGIs.")
4815    parser.add_argument("--close-all", "--cancel-all", type=str, nargs="*", help="Action: close all available (not blocked) opened trades and orders, excluding for currencies. Also you can select one or more keywords case insensitive to specify trades type: `orders`, `shares`, `bonds`, `etfs` and `futures`, but not `currencies`. Currency positions you must closes manually using `--buy`, `--sell`, `--close-trade` or `--close-trades` operations. If the `--close-all` key present with the `--ticker` or `--figi` keys, then positions and all open limit and stop orders for the specified instrument are closed.")
4816
4817    parser.add_argument("--limits", "--withdrawal-limits", "-w", action="store_true", help="Action: show table of funds available for withdrawal for current `accountId`. You can change `accountId` with the key `--account-id`. Also, you can define `--output` key to save this information to file, default: `limits.md`.")
4818    parser.add_argument("--user-info", "-u", action="store_true", help="Action: show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). Also, you can define `--output` key to save this information to file, default: `user-info.md`.")
4819    parser.add_argument("--account", "--accounts", "-a", action="store_true", help="Action: show simple table with all available user accounts. Also, you can define `--output` key to save this information to file, default: `accounts.md`.")
4820
4821    cmdArgs = parser.parse_args()
4822    return cmdArgs
4823
4824
4825def Main(**kwargs):
4826    """
4827    Main function for work with TKSBrokerAPI in the console.
4828
4829    See examples:
4830    - in english: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md
4831    - in russian: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README.md
4832    """
4833    args = Args(**kwargs) if kwargs else ParseArgs()  # get and parse command-line parameters or use **kwarg parameters
4834
4835    if args.debug_level:
4836        uLogger.level = 10  # always debug level by default
4837        uLogger.handlers[0].level = args.debug_level  # level for STDOUT
4838
4839    exitCode = 0
4840    start = datetime.now(tzutc())
4841    uLogger.debug("=-" * 50)
4842    uLogger.debug(">>> TKSBrokerAPI module started at: [{}] UTC, it is [{}] local time".format(
4843        start.strftime(TKS_PRINT_DATE_TIME_FORMAT),
4844        start.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4845    ))
4846
4847    # trying to calculate full current version:
4848    buildVersion = __version__
4849    try:
4850        v = version("tksbrokerapi")
4851        buildVersion = v if v.startswith(buildVersion) else buildVersion + ".dev0"  # set version as major.minor.dev0 if run as local build or local script
4852
4853    except Exception:
4854        buildVersion = __version__ + ".dev0"  # if an errors occurred then also set version as major.minor.dev0
4855
4856    uLogger.debug("TKSBrokerAPI major.minor.build version used: [{}]".format(buildVersion))
4857    uLogger.debug("Host CPU count: [{}]".format(CPU_COUNT))
4858
4859    try:
4860        if args.version:
4861            print("TKSBrokerAPI {}".format(buildVersion))
4862            uLogger.debug("User requested current TKSBrokerAPI major.minor.build version: [{}]".format(buildVersion))
4863
4864        else:
4865            # Init class for trading with Tinkoff Broker:
4866            trader = TinkoffBrokerServer(
4867                token=args.token,
4868                accountId=args.account_id,
4869                useCache=not args.no_cache,
4870            )
4871
4872            # --- set some options:
4873
4874            if args.more:
4875                trader.moreDebug = True
4876                uLogger.warning("More debug info mode is enabled! See network requests, responses and its headers in the full log or run TKSBrokerAPI platform with the `--verbosity 10` to show theres in console.")
4877
4878            if args.html:
4879                trader.useHTMLReports = True
4880
4881            if args.ticker:
4882                ticker = str(args.ticker).upper()  # Tickers may be upper case only
4883
4884                if ticker in trader.aliasesKeys:
4885                    trader.ticker = trader.aliases[ticker]  # Replace some tickers with its aliases
4886
4887                else:
4888                    trader.ticker = ticker
4889
4890            if args.figi:
4891                trader.figi = str(args.figi).upper()  # FIGIs may be upper case only
4892
4893            if args.depth is not None:
4894                trader.depth = args.depth
4895
4896            # --- do one command:
4897
4898            if args.list:
4899                if args.output is not None:
4900                    trader.instrumentsFile = args.output
4901
4902                trader.ShowInstrumentsInfo(show=True)
4903
4904            elif args.list_xlsx:
4905                trader.DumpInstrumentsAsXLSX(forceUpdate=False)
4906
4907            elif args.bonds_xlsx is not None:
4908                if args.output is not None:
4909                    trader.bondsXLSXFile = args.output
4910
4911                if len(args.bonds_xlsx) == 0:
4912                    trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=True)  # request bonds with all available tickers
4913
4914                else:
4915                    trader.ExtendBondsData(instruments=args.bonds_xlsx, xlsx=True)  # request list of given bonds
4916
4917            elif args.search:
4918                if args.output is not None:
4919                    trader.searchResultsFile = args.output
4920
4921                trader.SearchInstruments(pattern=args.search[0], show=True)
4922
4923            elif args.info:
4924                if not (args.ticker or args.figi):
4925                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
4926                    raise Exception("Ticker or FIGI required")
4927
4928                if args.output is not None:
4929                    trader.infoFile = args.output
4930
4931                if args.ticker:
4932                    trader.SearchByTicker(requestPrice=True, show=True)  # show info and current prices by ticker name
4933
4934                else:
4935                    trader.SearchByFIGI(requestPrice=True, show=True)  # show info and current prices by FIGI id
4936
4937            elif args.calendar is not None:
4938                if args.output is not None:
4939                    trader.calendarFile = args.output
4940
4941                if len(args.calendar) == 0:
4942                    bondsData = trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=False)  # request bonds with all available tickers
4943
4944                else:
4945                    bondsData = trader.ExtendBondsData(instruments=args.calendar, xlsx=False)  # request list of given bonds
4946
4947                trader.ShowBondsCalendar(extBonds=bondsData, show=True)  # shows bonds payment calendar only
4948
4949            elif args.price:
4950                if not (args.ticker or args.figi):
4951                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
4952                    raise Exception("Ticker or FIGI required")
4953
4954                trader.GetCurrentPrices(show=True)
4955
4956            elif args.prices is not None:
4957                if args.output is not None:
4958                    trader.pricesFile = args.output
4959
4960                trader.GetListOfPrices(instruments=args.prices, show=True)  # WARNING: too long wait for a lot of instruments prices
4961
4962            elif args.overview:
4963                if args.output is not None:
4964                    trader.overviewFile = args.output
4965
4966                trader.Overview(show=True, details="full")
4967
4968            elif args.overview_digest:
4969                if args.output is not None:
4970                    trader.overviewDigestFile = args.output
4971
4972                trader.Overview(show=True, details="digest")
4973
4974            elif args.overview_positions:
4975                if args.output is not None:
4976                    trader.overviewPositionsFile = args.output
4977
4978                trader.Overview(show=True, details="positions")
4979
4980            elif args.overview_orders:
4981                if args.output is not None:
4982                    trader.overviewOrdersFile = args.output
4983
4984                trader.Overview(show=True, details="orders")
4985
4986            elif args.overview_analytics:
4987                if args.output is not None:
4988                    trader.overviewAnalyticsFile = args.output
4989
4990                trader.Overview(show=True, details="analytics")
4991
4992            elif args.overview_calendar:
4993                if args.output is not None:
4994                    trader.overviewAnalyticsFile = args.output
4995
4996                trader.Overview(show=True, details="calendar")
4997
4998            elif args.deals is not None:
4999                if args.output is not None:
5000                    trader.reportFile = args.output
5001
5002                if 0 <= len(args.deals) < 3:
5003                    trader.Deals(
5004                        start=args.deals[0] if len(args.deals) >= 1 else None,
5005                        end=args.deals[1] if len(args.deals) == 2 else None,
5006                        show=True,  # Always show deals report in console
5007                        showCancelled=not args.no_cancelled,  # If --no-cancelled key then remove cancelled operations from the deals report. False by default.
5008                    )
5009
5010                else:
5011                    uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]")
5012                    raise Exception("Incorrect value")
5013
5014            elif args.history is not None:
5015                if args.output is not None:
5016                    trader.historyFile = args.output
5017
5018                if 0 <= len(args.history) < 3:
5019                    dataReceived = trader.History(
5020                        start=args.history[0] if len(args.history) >= 1 else None,
5021                        end=args.history[1] if len(args.history) == 2 else None,
5022                        interval="hour" if args.interval is None or not args.interval else args.interval,
5023                        onlyMissing=False if args.only_missing is None or not args.only_missing else args.only_missing,
5024                        csvSep="," if args.csv_sep is None or not args.csv_sep else args.csv_sep,
5025                        show=True,  # shows all downloaded candles in console
5026                    )
5027
5028                    if args.render_chart is not None and dataReceived is not None:
5029                        iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True
5030
5031                        trader.ShowHistoryChart(
5032                            candles=dataReceived,
5033                            interact=iChart,
5034                            openInBrowser=False,  # False by default, to avoid issues with `permissions denied` to html-file.
5035                        )
5036
5037                else:
5038                    uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]")
5039                    raise Exception("Incorrect value")
5040
5041            elif args.load_history is not None:
5042                histData = trader.LoadHistory(filePath=args.load_history)  # load data from file and show history in console
5043
5044                if args.render_chart is not None and histData is not None:
5045                    iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True
5046                    trader.ticker = os.path.basename(args.load_history)  # use filename as ticker name for PriceGenerator's chart
5047
5048                    trader.ShowHistoryChart(
5049                        candles=histData,
5050                        interact=iChart,
5051                        openInBrowser=False,  # False by default, to avoid issues with `permissions denied` to html-file.
5052                    )
5053
5054            elif args.trade is not None:
5055                if 1 <= len(args.trade) <= 5:
5056                    trader.Trade(
5057                        operation=args.trade[0],
5058                        lots=int(args.trade[1]) if len(args.trade) >= 2 else 1,
5059                        tp=float(args.trade[2]) if len(args.trade) >= 3 else 0.,
5060                        sl=float(args.trade[3]) if len(args.trade) >= 4 else 0.,
5061                        expDate=args.trade[4] if len(args.trade) == 5 else "Undefined",
5062                    )
5063
5064                else:
5065                    uLogger.error("You must specify 1-5 parameters to open trade: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
5066
5067            elif args.buy is not None:
5068                if 0 <= len(args.buy) <= 4:
5069                    trader.Buy(
5070                        lots=int(args.buy[0]) if len(args.buy) >= 1 else 1,
5071                        tp=float(args.buy[1]) if len(args.buy) >= 2 else 0.,
5072                        sl=float(args.buy[2]) if len(args.buy) >= 3 else 0.,
5073                        expDate=args.buy[3] if len(args.buy) == 4 else "Undefined",
5074                    )
5075
5076                else:
5077                    uLogger.error("You must specify 0-4 parameters to open buy position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
5078
5079            elif args.sell is not None:
5080                if 0 <= len(args.sell) <= 4:
5081                    trader.Sell(
5082                        lots=int(args.sell[0]) if len(args.sell) >= 1 else 1,
5083                        tp=float(args.sell[1]) if len(args.sell) >= 2 else 0.,
5084                        sl=float(args.sell[2]) if len(args.sell) >= 3 else 0.,
5085                        expDate=args.sell[3] if len(args.sell) == 4 else "Undefined",
5086                    )
5087
5088                else:
5089                    uLogger.error("You must specify 0-4 parameters to open sell position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
5090
5091            elif args.order:
5092                if 4 <= len(args.order) <= 7:
5093                    trader.Order(
5094                        operation=args.order[0],
5095                        orderType=args.order[1],
5096                        lots=int(args.order[2]),
5097                        targetPrice=float(args.order[3]),
5098                        limitPrice=float(args.order[4]) if len(args.order) >= 5 else 0.,
5099                        stopType=args.order[5] if len(args.order) >= 6 else "Limit",
5100                        expDate=args.order[6] if len(args.order) == 7 else "Undefined",
5101                    )
5102
5103                else:
5104                    uLogger.error("You must specify 4-7 parameters to open order: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]]. See: `python TKSBrokerAPI.py --help`")
5105
5106            elif args.buy_limit:
5107                trader.BuyLimit(lots=int(args.buy_limit[0]), targetPrice=args.buy_limit[1])
5108
5109            elif args.sell_limit:
5110                trader.SellLimit(lots=int(args.sell_limit[0]), targetPrice=args.sell_limit[1])
5111
5112            elif args.buy_stop:
5113                if 2 <= len(args.buy_stop) <= 7:
5114                    trader.BuyStop(
5115                        lots=int(args.buy_stop[0]),
5116                        targetPrice=float(args.buy_stop[1]),
5117                        limitPrice=float(args.buy_stop[2]) if len(args.buy_stop) >= 3 else 0.,
5118                        stopType=args.buy_stop[3] if len(args.buy_stop) >= 4 else "Limit",
5119                        expDate=args.buy_stop[4] if len(args.buy_stop) == 5 else "Undefined",
5120                    )
5121
5122                else:
5123                    uLogger.error("You must specify 2-5 parameters for buy stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
5124
5125            elif args.sell_stop:
5126                if 2 <= len(args.sell_stop) <= 7:
5127                    trader.SellStop(
5128                        lots=int(args.sell_stop[0]),
5129                        targetPrice=float(args.sell_stop[1]),
5130                        limitPrice=float(args.sell_stop[2]) if len(args.sell_stop) >= 3 else 0.,
5131                        stopType=args.sell_stop[3] if len(args.sell_stop) >= 4 else "Limit",
5132                        expDate=args.sell_stop[4] if len(args.sell_stop) == 5 else "Undefined",
5133                    )
5134
5135                else:
5136                    uLogger.error("You must specify 2-5 parameters for sell stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: python TKSBrokerAPI.py --help")
5137
5138            # elif args.buy_order_grid is not None:
5139            #     # update order grid work with api v2
5140            #     if len(args.buy_order_grid) == 2:
5141            #         orderParams = trader.ParseOrderParameters(operation="Buy", **dict(kw.split('=') for kw in args.buy_order_grid))
5142            #
5143            #         for order in orderParams:
5144            #             trader.Order(operation="Buy", lots=order["lot"], price=order["price"])
5145            #
5146            #     else:
5147            #         uLogger.error("To open grid of pending BUY limit-orders (below current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`")
5148            #
5149            # elif args.sell_order_grid is not None:
5150            #     # update order grid work with api v2
5151            #     if len(args.sell_order_grid) >= 2:
5152            #         orderParams = trader.ParseOrderParameters(operation="Sell", **dict(kw.split('=') for kw in args.sell_order_grid))
5153            #
5154            #         for order in orderParams:
5155            #             trader.Order(operation="Sell", lots=order["lot"], price=order["price"])
5156            #
5157            #     else:
5158            #         uLogger.error("To open grid of pending SELL limit-orders (above current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`")
5159
5160            elif args.close_order is not None:
5161                trader.CloseOrders(args.close_order)  # close only one order
5162
5163            elif args.close_orders is not None:
5164                trader.CloseOrders(args.close_orders)  # close list of orders
5165
5166            elif args.close_trade:
5167                if not (args.ticker or args.figi):
5168                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
5169                    raise Exception("Ticker or FIGI required")
5170
5171                if args.ticker:
5172                    trader.CloseTrades([str(args.ticker).upper()])  # close only one trade by ticker (priority)
5173
5174                else:
5175                    trader.CloseTrades([str(args.figi).upper()])  # close only one trade by FIGI
5176
5177            elif args.close_trades is not None:
5178                trader.CloseTrades(args.close_trades)  # close trades for list of tickers
5179
5180            elif args.close_all is not None:
5181                if args.ticker:
5182                    trader.CloseAllByTicker(instrument=str(args.ticker).upper())
5183
5184                elif args.figi:
5185                    trader.CloseAllByFIGI(instrument=str(args.figi).upper())
5186
5187                else:
5188                    trader.CloseAll(*args.close_all)
5189
5190            elif args.limits:
5191                if args.output is not None:
5192                    trader.withdrawalLimitsFile = args.output
5193
5194                trader.OverviewLimits(show=True)
5195
5196            elif args.user_info:
5197                if args.output is not None:
5198                    trader.userInfoFile = args.output
5199
5200                trader.OverviewUserInfo(show=True)
5201
5202            elif args.account:
5203                if args.output is not None:
5204                    trader.userAccountsFile = args.output
5205
5206                trader.OverviewAccounts(show=True)
5207
5208            else:
5209                uLogger.error("There is no command to execute! One of the possible commands must be selected. See help with `--help` key.")
5210                raise Exception("There is no command to execute")
5211
5212    except Exception:
5213        trace = tb.format_exc()
5214        for e in ["socket.gaierror", "nodename nor servname provided", "or not known", "NewConnectionError", "[Errno 8]", "Failed to establish a new connection"]:
5215            if e in trace:
5216                uLogger.error("Check your Internet connection! Failed to establish connection to broker server!")
5217                break
5218
5219        uLogger.debug(trace)
5220        uLogger.debug("Please, check issues or request a new one at https://github.com/Tim55667757/TKSBrokerAPI/issues")
5221        exitCode = 255  # an error occurred, must be open a ticket for this issue
5222
5223    finally:
5224        finish = datetime.now(tzutc())
5225
5226        if exitCode == 0:
5227            if args.more:
5228                uLogger.debug("All operations were finished success (summary code is 0).")
5229
5230        else:
5231            uLogger.error("An issue occurred with TKSBrokerAPI module! See full debug log in [{}] or run TKSBrokerAPI once again with the key `--debug-level 10`. Summary code: {}".format(
5232                os.path.abspath(uLog.defaultLogFile), exitCode,
5233            ))
5234
5235        uLogger.debug(">>> TKSBrokerAPI module work duration: [{}]".format(finish - start))
5236        uLogger.debug(">>> TKSBrokerAPI module finished: [{} UTC], it is [{}] local time".format(
5237            finish.strftime(TKS_PRINT_DATE_TIME_FORMAT),
5238            finish.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
5239        ))
5240        uLogger.debug("=-" * 50)
5241
5242        if not kwargs:
5243            sys.exit(exitCode)
5244
5245        else:
5246            return exitCode
5247
5248
5249if __name__ == "__main__":
5250    Main()
class TinkoffBrokerServer:
  78class TinkoffBrokerServer:
  79    """
  80    This class implements methods to work with Tinkoff broker server.
  81
  82    Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/
  83
  84    About `token`: https://tinkoff.github.io/investAPI/token/
  85    """
  86    def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None:
  87        """
  88        Main class init.
  89
  90        :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`.
  91        :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports.
  92                          Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.
  93        :param useCache: use default cache file with raw data to use instead of `iList`.
  94                         True by default. Cache is auto-update if new day has come.
  95                         If you don't want to use cache and always updates raw data then set `useCache=False`.
  96        :param defaultCache: path to default cache file. `dump.json` by default.
  97        """
  98        if token is None or not token:
  99            try:
 100                self.token = r"{}".format(os.environ["TKS_API_TOKEN"])
 101                uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/")
 102
 103            except KeyError:
 104                uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/")
 105                raise Exception("Token required")
 106
 107        else:
 108            self.token = token  # highly priority than environment variable 'TKS_API_TOKEN'
 109            uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`")
 110
 111        if accountId is None or not accountId:
 112            try:
 113                self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"])
 114                uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId))
 115
 116            except KeyError:
 117                uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).")
 118
 119        else:
 120            self.accountId = accountId  # highly priority than environment variable 'TKS_ACCOUNT_ID'
 121            uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId))
 122
 123        self.version = __version__  # duplicate here used TKSBrokerAPI main version
 124        """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.
 125
 126        Latest version: https://pypi.org/project/tksbrokerapi/
 127        """
 128
 129        self.__lock = Lock()  # initialize multiprocessing mutex lock
 130
 131        self.aliases = TKS_TICKER_ALIASES
 132        """Some aliases instead official tickers.
 133
 134        See also: `TKSEnums.TKS_TICKER_ALIASES`
 135        """
 136
 137        self.aliasesKeys = self.aliases.keys()  # re-calc only first time at class init
 138
 139        self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED  # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there
 140
 141        self._ticker = ""
 142        """String with ticker, e.g. `GOOGL`. Tickers may be upper case only.
 143
 144        Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc.
 145        More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
 146
 147        See also: `SearchByTicker()`, `SearchInstruments()`.
 148        """
 149
 150        self._figi = ""
 151        """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only.
 152
 153        See also: `SearchByFIGI()`, `SearchInstruments()`.
 154        """
 155
 156        self.depth = 1
 157        """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI.
 158
 159        See also: `GetCurrentPrices()`.
 160        """
 161
 162        self.server = r"https://invest-public-api.tinkoff.ru/rest"
 163        """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest
 164
 165        See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`.
 166        """
 167
 168        uLogger.debug("Broker API server: {}".format(self.server))
 169
 170        self.timeout = 15
 171        """Server operations timeout in seconds. Default: `15`.
 172
 173        See also: `SendAPIRequest()`.
 174        """
 175
 176        self.headers = {
 177            "Content-Type": "application/json",
 178            "accept": "application/json",
 179            "Authorization": "Bearer {}".format(self.token),
 180            "x-app-name": "Tim55667757.TKSBrokerAPI",
 181        }
 182        """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`.
 183
 184        See also: `SendAPIRequest()`.
 185        """
 186
 187        self.body = None
 188        """Request body which send to broker server. Default: `None`.
 189
 190        See also: `SendAPIRequest()`.
 191        """
 192
 193        self.moreDebug = False
 194        """Enables more debug information in this class, such as net request and response headers in all methods. `False` by default."""
 195
 196        self.useHTMLReports = False
 197        """
 198        If `True` then TKSBrokerAPI generate also HTML reports from Markdown. `False` by default.
 199        
 200        See also: Mako Templates for Python (https://www.makotemplates.org/). Mako is a template library provides simple syntax and maximum performance.
 201        """
 202
 203        self.historyFile = None
 204        """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame.
 205
 206        See also: `History()`.
 207        """
 208
 209        self.htmlHistoryFile = "index.html"
 210        """Full path to the html file where rendered candles chart stored. Default: `index.html`.
 211
 212        See also: `ShowHistoryChart()`.
 213        """
 214
 215        self.instrumentsFile = "instruments.md"
 216        """Filename where full available to user instruments list will be saved. Default: `instruments.md`.
 217
 218        See also: `ShowInstrumentsInfo()`.
 219        """
 220
 221        self.searchResultsFile = "search-results.md"
 222        """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`.
 223
 224        See also: `SearchInstruments()`.
 225        """
 226
 227        self.pricesFile = "prices.md"
 228        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
 229
 230        See also: `GetListOfPrices()`.
 231        """
 232
 233        self.infoFile = "info.md"
 234        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
 235
 236        See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`.
 237        """
 238
 239        self.bondsXLSXFile = "ext-bonds.xlsx"
 240        """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 
 241        bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`.
 242
 243        See also: `ExtendBondsData()`.
 244        """
 245
 246        self.calendarFile = "calendar.md"
 247        """Filename where bonds payment calendar will be saved. Default: `calendar.md`.
 248        
 249        Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`.
 250
 251        See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`.
 252        """
 253
 254        self.overviewFile = "overview.md"
 255        """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`.
 256
 257        See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`.
 258        """
 259
 260        self.overviewDigestFile = "overview-digest.md"
 261        """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`.
 262
 263        See also: `Overview()` with parameter `details="digest"`.
 264        """
 265
 266        self.overviewPositionsFile = "overview-positions.md"
 267        """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`.
 268
 269        See also: `Overview()` with parameter `details="positions"`.
 270        """
 271
 272        self.overviewOrdersFile = "overview-orders.md"
 273        """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`.
 274
 275        See also: `Overview()` with parameter `details="orders"`.
 276        """
 277
 278        self.overviewAnalyticsFile = "overview-analytics.md"
 279        """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`.
 280
 281        See also: `Overview()` with parameter `details="analytics"`.
 282        """
 283
 284        self.overviewBondsCalendarFile = "overview-calendar.md"
 285        """Filename where only the bonds calendar section will be saved. Default: `overview-calendar.md`.
 286
 287        See also: `Overview()` with parameter `details="calendar"`.
 288        """
 289
 290        self.reportFile = "deals.md"
 291        """Filename where history of deals and trade statistics will be saved. Default: `deals.md`.
 292
 293        See also: `Deals()`.
 294        """
 295
 296        self.withdrawalLimitsFile = "limits.md"
 297        """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`.
 298
 299        See also: `OverviewLimits()` and `RequestLimits()`.
 300        """
 301
 302        self.userInfoFile = "user-info.md"
 303        """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`.
 304
 305        See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`.
 306        """
 307
 308        self.userAccountsFile = "accounts.md"
 309        """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`.
 310
 311        See also: `OverviewAccounts()`, `RequestAccounts()`.
 312        """
 313
 314        self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache
 315        """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`.
 316
 317        Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`.
 318
 319        See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`.
 320        """
 321
 322        self.iList = None  # init iList for raw instruments data
 323        """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`.
 324        
 325        See also: `Listing()`, `DumpInstruments()`.
 326        """
 327
 328        # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server:
 329        if useCache:
 330            if os.path.exists(self.iListDumpFile):
 331                dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc())  # dump modification date and time
 332                curTime = datetime.now(tzutc())
 333
 334                if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year):
 335                    uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
 336
 337                    self.DumpInstruments(forceUpdate=True)  # updating self.iList and dump file
 338
 339                else:
 340                    self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8"))  # load iList from dump
 341
 342                    uLogger.debug("Local cache with raw instruments data is used: [{}]. Last modified: [{}] UTC".format(
 343                        os.path.abspath(self.iListDumpFile),
 344                        dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT),
 345                    ))
 346
 347            else:
 348                uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...")
 349                self.DumpInstruments(forceUpdate=True)  # updating self.iList and creating default dump file
 350
 351        else:
 352            self.iList = self.Listing()  # request new raw instruments data from broker server
 353            self.DumpInstruments(forceUpdate=False)  # save raw instrument's data to default dump file `iListDumpFile`
 354
 355        self.priceModel = PriceGenerator()  # init PriceGenerator object to work with candles data
 356        """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.
 357
 358        See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator
 359        """
 360
 361    @property
 362    def ticker(self) -> str:
 363        """String with ticker, e.g. `GOOGL`. Tickers may be upper case only.
 364
 365        Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc.
 366        More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
 367
 368        See also: `SearchByTicker()`, `SearchInstruments()`.
 369        """
 370        return self._ticker
 371
 372    @ticker.setter
 373    def ticker(self, value):
 374        """Setter for string with ticker, e.g. `GOOGL`. Tickers may be upper case only.
 375
 376        Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc.
 377        More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
 378
 379        See also: `SearchByTicker()`, `SearchInstruments()`.
 380        """
 381        self._ticker = str(value).upper()  # Tickers may be upper case only
 382
 383    @property
 384    def figi(self) -> str:
 385        """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only.
 386
 387        See also: `SearchByFIGI()`, `SearchInstruments()`.
 388        """
 389        return self._figi
 390
 391    @figi.setter
 392    def figi(self, value):
 393        """Setter for string with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only.
 394
 395        See also: `SearchByFIGI()`, `SearchInstruments()`.
 396        """
 397        self._figi = str(value).upper()  # FIGI may be upper case only
 398
 399    def _ParseJSON(self, rawData="{}") -> dict:
 400        """
 401        Parse JSON from response string.
 402
 403        :param rawData: this is a string with JSON-formatted text.
 404        :return: JSON (dictionary), parsed from server response string. If an error occurred, then returns empty dict `{}`.
 405        """
 406        try:
 407            responseJSON = json.loads(rawData) if rawData else {}
 408
 409            if self.moreDebug:
 410                uLogger.debug("JSON formatted raw body data of response:\n{}".format(json.dumps(responseJSON, indent=4)))
 411
 412            return responseJSON
 413
 414        except Exception as e:
 415            uLogger.error("An empty dict will be return, because an error occurred in `_ParseJSON()` method with comment: {}".format(e))
 416            return {}
 417
 418    def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5) -> dict:
 419        """
 420        Send GET or POST request to broker server and receive JSON object.
 421
 422        self.header: must be defining with dictionary of headers.
 423        self.body: if define then used as request body. None by default.
 424        self.timeout: global request timeout, 15 seconds by default.
 425        :param url: url with REST request.
 426        :param reqType: send "GET" or "POST" request. "GET" by default.
 427        :param retry: how many times retry after first request if an 5xx server errors occurred.
 428        :param pause: sleep time in seconds between retries.
 429        :return: response JSON (dictionary) from broker.
 430        """
 431        if reqType.upper() not in ("GET", "POST"):
 432            uLogger.error("You can define request type: `GET` or `POST`!")
 433            raise Exception("Incorrect value")
 434
 435        if self.moreDebug:
 436            uLogger.debug("Request parameters:")
 437            uLogger.debug("    - REST API URL: {}".format(url))
 438            uLogger.debug("    - request type: {}".format(reqType))
 439            uLogger.debug("    - headers:\n{}".format(str(self.headers).replace(self.token, "*** request token ***")))
 440            uLogger.debug("    - body:\n{}".format(self.body))
 441
 442        # fast hack to avoid all operations with some tickers/FIGI
 443        responseJSON = {}
 444        oK = True
 445        for item in self.exclude:
 446            if item in url:
 447                if self.moreDebug:
 448                    uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude)))
 449
 450                oK = False
 451                break
 452
 453        if oK:
 454            with self.__lock:  # acquire the mutex lock
 455                counter = 0
 456                response = None
 457                errMsg = ""
 458
 459                while not response and counter <= retry:
 460                    if reqType == "GET":
 461                        response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout)
 462
 463                    if reqType == "POST":
 464                        response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout)
 465
 466                    if self.moreDebug:
 467                        uLogger.debug("Response:")
 468                        uLogger.debug("    - status code: {}".format(response.status_code))
 469                        uLogger.debug("    - reason: {}".format(response.reason))
 470                        uLogger.debug("    - body length: {}".format(len(response.text)))
 471                        uLogger.debug("    - headers:\n{}".format(response.headers))
 472
 473                    # Server returns some headers:
 474                    # - `x-ratelimit-limit` — shows the settings of the current user limit for this method.
 475                    # - `x-ratelimit-remaining` — the number of remaining requests of this type per minute.
 476                    # - `x-ratelimit-reset` — time in seconds before resetting the request counter.
 477                    # See: https://tinkoff.github.io/investAPI/grpc/#kreya
 478                    if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0":
 479                        rateLimitWait = int(response.headers["x-ratelimit-reset"])
 480                        uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait))
 481                        sleep(rateLimitWait)
 482
 483                    # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes
 484                    if 400 <= response.status_code < 500:
 485                        msg = "status code: [{}], response body: {}".format(response.status_code, response.text)
 486                        uLogger.debug("    - not oK, but do not retry for 4xx errors, {}".format(msg))
 487
 488                        if "code" in response.text and "message" in response.text:
 489                            msgDict = self._ParseJSON(rawData=response.text)
 490                            uLogger.warning("HTTP-status code [{}], server message: {}".format(response.status_code, msgDict["message"]))
 491
 492                        counter = retry + 1  # do not retry for 4xx errors
 493
 494                    if 500 <= response.status_code < 600:
 495                        errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text)
 496                        uLogger.debug("    - not oK, {}".format(errMsg))
 497
 498                        if "code" in response.text and "message" in response.text:
 499                            errMsgDict = self._ParseJSON(rawData=response.text)
 500                            uLogger.warning("HTTP-status code [{}], error message: {}".format(response.status_code, errMsgDict["message"]))
 501
 502                        counter += 1
 503
 504                        if counter <= retry:
 505                            uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause))
 506                            sleep(pause)
 507
 508                responseJSON = self._ParseJSON(rawData=response.text)
 509
 510                if errMsg:
 511                    uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/")
 512                    uLogger.error("    - not oK, {}".format(errMsg))
 513
 514        return responseJSON
 515
 516    def _IUpdater(self, iType: str) -> tuple:
 517        """
 518        Request instrument by type from server. See available API methods for instruments:
 519        Currencies: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Currencies
 520        Shares: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Shares
 521        Bonds: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Bonds
 522        Etfs: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Etfs
 523        Futures: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Futures
 524
 525        :param iType: type of the instrument, it must be one of supported types in TKS_INSTRUMENTS list.
 526        :return: tuple with iType name and list of available instruments of current type for defined user token.
 527        """
 528        result = []
 529
 530        if iType in TKS_INSTRUMENTS:
 531            uLogger.debug("Requesting available [{}] list. Wait, please...".format(iType))
 532
 533            # all instruments have the same body in API v2 requests:
 534            self.body = str({"instrumentStatus": "INSTRUMENT_STATUS_UNSPECIFIED"})  # Enum: [INSTRUMENT_STATUS_UNSPECIFIED, INSTRUMENT_STATUS_BASE, INSTRUMENT_STATUS_ALL]
 535            instrumentURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/{}".format(iType)
 536            result = self.SendAPIRequest(instrumentURL, reqType="POST")["instruments"]
 537
 538        return iType, result
 539
 540    def _IWrapper(self, kwargs):
 541        """
 542        Wrapper runs instrument's update method `_IUpdater()`.
 543        It's a workaround for using multiprocessing with kwargs. See: https://stackoverflow.com/a/36799206
 544        """
 545        return self._IUpdater(**kwargs)
 546
 547    def Listing(self) -> dict:
 548        """
 549        Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.
 550
 551        :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.
 552        """
 553        uLogger.debug("Requesting all available instruments for current account. Wait, please...")
 554        uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES))
 555
 556        # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService
 557        # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list.
 558        iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS]
 559
 560        poolUpdater = ThreadPool(processes=CPU_USAGES)  # create pool for update instruments in parallel mode
 561        listing = poolUpdater.map(self._IWrapper, iParams)  # execute update operations
 562        poolUpdater.close()  # close the thread pool
 563        poolUpdater.join()  # wait a moment until all data returns from threads
 564
 565        # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures.
 566        # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method
 567        iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing}
 568
 569        # calculate minimum price increment (step) for all instruments and set up instrument's type:
 570        for iType in iList.keys():
 571            for ticker in iList[iType]:
 572                iList[iType][ticker]["type"] = iType
 573
 574                if "minPriceIncrement" in iList[iType][ticker].keys():
 575                    iList[iType][ticker]["step"] = NanoToFloat(
 576                        iList[iType][ticker]["minPriceIncrement"]["units"],
 577                        iList[iType][ticker]["minPriceIncrement"]["nano"],
 578                    )
 579
 580                else:
 581                    iList[iType][ticker]["step"] = 0  # hack to avoid empty value in some instruments, e.g. futures
 582
 583        return iList
 584
 585    def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None:
 586        """
 587        Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.
 588
 589        See also: `DumpInstruments()`, `Listing()`.
 590
 591        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
 592                            otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) .
 593        """
 594        if self.iListDumpFile is None or not self.iListDumpFile:
 595            uLogger.error("Output name of dump file must be defined!")
 596            raise Exception("Filename required")
 597
 598        if not self.iList or forceUpdate:
 599            self.iList = self.Listing()
 600
 601        xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx"
 602
 603        # Save as XLSX with separated sheets for every type of instruments:
 604        with pd.ExcelWriter(
 605                path=xlsxDumpFile,
 606                date_format=TKS_DATE_FORMAT,
 607                datetime_format=TKS_DATE_TIME_FORMAT,
 608                mode="w",
 609        ) as writer:
 610            for iType in TKS_INSTRUMENTS:
 611                df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index")  # generate pandas object from self.iList dictionary
 612                df = df[sorted(df)]  # sorted by column names
 613                df = df.applymap(
 614                    lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item,
 615                    na_action="ignore",
 616                )  # converting numbers from nano-type to float in every cell
 617                df.to_excel(
 618                    writer,
 619                    sheet_name=iType,
 620                    encoding="UTF-8",
 621                    freeze_panes=(1, 1),
 622                )  # saving as XLSX-file with freeze first row and column as headers
 623
 624        uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile)))
 625
 626    def DumpInstruments(self, forceUpdate: bool = True) -> str:
 627        """
 628        Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server
 629        using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file.
 630
 631        See also: `DumpInstrumentsAsXLSX()`, `Listing()`.
 632
 633        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
 634                            otherwise just saves exist `iList` as JSON-file (default: `dump.json`).
 635        :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file.
 636        """
 637        if self.iListDumpFile is None or not self.iListDumpFile:
 638            uLogger.error("Output name of dump file must be defined!")
 639            raise Exception("Filename required")
 640
 641        if not self.iList or forceUpdate:
 642            self.iList = self.Listing()
 643
 644        jsonDump = json.dumps(self.iList, indent=4, sort_keys=False)  # create JSON object as string
 645        with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH:
 646            fH.write(jsonDump)
 647
 648        uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile)))
 649
 650        return jsonDump
 651
 652    def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str:
 653        """
 654        Show information about one instrument defined by json data and prints it in Markdown format.
 655
 656        See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`.
 657
 658        :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self._ticker]`
 659        :param show: if `True` then also printing information about instrument and its current price.
 660        :return: multilines text in Markdown format with information about one instrument.
 661        """
 662        splitLine = "|                                                             |                                                        |\n"
 663        infoText = ""
 664
 665        if iJSON is not None and iJSON and isinstance(iJSON, dict):
 666            info = [
 667                "# Main information\n\n",
 668                "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
 669                "| Parameters                                                  | Values                                                 |\n",
 670                "|-------------------------------------------------------------|--------------------------------------------------------|\n",
 671                "| Ticker:                                                     | {:<54} |\n".format(iJSON["ticker"]),
 672                "| Full name:                                                  | {:<54} |\n".format(iJSON["name"]),
 673            ]
 674
 675            if "sector" in iJSON.keys() and iJSON["sector"]:
 676                info.append("| Sector:                                                     | {:<54} |\n".format(iJSON["sector"]))
 677
 678            if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] and "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"]:
 679                info.append("| Country of instrument:                                      | {:<54} |\n".format("({}) {}".format(iJSON["countryOfRisk"], iJSON["countryOfRiskName"])))
 680
 681            info.extend([
 682                splitLine,
 683                "| FIGI (Financial Instrument Global Identifier):              | {:<54} |\n".format(iJSON["figi"]),
 684                "| Real exchange [Exchange section]:                           | {:<54} |\n".format("{} [{}]".format(TKS_REAL_EXCHANGES[iJSON["realExchange"]], iJSON["exchange"])),
 685            ])
 686
 687            if "isin" in iJSON.keys() and iJSON["isin"]:
 688                info.append("| ISIN (International Securities Identification Number):      | {:<54} |\n".format(iJSON["isin"]))
 689
 690            if "classCode" in iJSON.keys():
 691                info.append("| Class Code (exchange section where instrument is traded):   | {:<54} |\n".format(iJSON["classCode"]))
 692
 693            info.extend([
 694                splitLine,
 695                "| Current broker security trading status:                     | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]),
 696                splitLine,
 697                "| Buy operations allowed:                                     | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"),
 698                "| Sale operations allowed:                                    | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"),
 699                "| Short positions allowed:                                    | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"),
 700            ])
 701
 702            if iJSON["figi"]:
 703                self._figi = iJSON["figi"]
 704                iJSON = iJSON | self.RequestTradingStatus()
 705
 706                info.extend([
 707                    splitLine,
 708                    "| Limit orders allowed:                                       | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"),
 709                    "| Market orders allowed:                                      | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"),
 710                    "| API trade allowed:                                          | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"),
 711                ])
 712
 713            info.append(splitLine)
 714
 715            if "type" in iJSON.keys() and iJSON["type"]:
 716                info.append("| Type of the instrument:                                     | {:<54} |\n".format(iJSON["type"]))
 717
 718                if "shareType" in iJSON.keys() and iJSON["shareType"]:
 719                    info.append("| Share type:                                                 | {:<54} |\n".format(TKS_SHARE_TYPES[iJSON["shareType"]]))
 720
 721            if "futuresType" in iJSON.keys() and iJSON["futuresType"]:
 722                info.append("| Futures type:                                               | {:<54} |\n".format(iJSON["futuresType"]))
 723
 724            if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]:
 725                info.append("| IPO date:                                                   | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", "")))
 726
 727            if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]:
 728                info.append("| Released date:                                              | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", "")))
 729
 730            if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]:
 731                info.append("| Rebalancing frequency:                                      | {:<54} |\n".format(iJSON["rebalancingFreq"]))
 732
 733            if "focusType" in iJSON.keys() and iJSON["focusType"]:
 734                info.append("| Focusing type:                                              | {:<54} |\n".format(iJSON["focusType"]))
 735
 736            if "assetType" in iJSON.keys() and iJSON["assetType"]:
 737                info.append("| Asset type:                                                 | {:<54} |\n".format(iJSON["assetType"]))
 738
 739            if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]:
 740                info.append("| Basic asset:                                                | {:<54} |\n".format(iJSON["basicAsset"]))
 741
 742            if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]:
 743                info.append("| Basic asset size:                                           | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"]))))
 744
 745            if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]:
 746                info.append("| ISO currency name:                                          | {:<54} |\n".format(iJSON["isoCurrencyName"]))
 747
 748            if "currency" in iJSON.keys():
 749                info.append("| Payment currency:                                           | {:<54} |\n".format(iJSON["currency"]))
 750
 751            if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys():
 752                info.append("| Nominal currency:                                           | {:<54} |\n".format(iJSON["nominal"]["currency"]))
 753
 754            if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]:
 755                info.append("| First trade date:                                           | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", "")))
 756
 757            if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]:
 758                info.append("| Last trade date:                                            | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", "")))
 759
 760            if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]:
 761                info.append("| Date of expiration:                                         | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", "")))
 762
 763            if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]:
 764                info.append("| State registration date:                                    | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", "")))
 765
 766            if "placementDate" in iJSON.keys() and iJSON["placementDate"]:
 767                info.append("| Placement date:                                             | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", "")))
 768
 769            if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]:
 770                info.append("| Maturity date:                                              | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", "")))
 771
 772            if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]:
 773                info.append("| Perpetual bond:                                             | Yes                                                    |\n")
 774
 775            if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]:
 776                info.append("| Over-the-counter (OTC) securities:                          | Yes                                                    |\n")
 777
 778            iExt = None
 779            if iJSON["type"] == "Bonds":
 780                info.extend([
 781                    splitLine,
 782                    "| Bond issue (size / plan):                                   | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])),
 783                    "| Nominal price (100%):                                       | {:<54} |\n".format("{} {}".format(
 784                        "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."),
 785                        iJSON["nominal"]["currency"],
 786                    )),
 787                ])
 788
 789                if "floatingCouponFlag" in iJSON.keys():
 790                    info.append("| Floating coupon:                                            | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No"))
 791
 792                if "amortizationFlag" in iJSON.keys():
 793                    info.append("| Amortization:                                               | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No"))
 794
 795                info.append(splitLine)
 796
 797                if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]:
 798                    info.append("| Number of coupon payments per year:                         | {:<54} |\n".format(iJSON["couponQuantityPerYear"]))
 799
 800                if iJSON["figi"]:
 801                    iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False)  # extended bonds data
 802
 803                    info.extend([
 804                        "| Days last to maturity date:                                 | {:<54} |\n".format(iExt["daysToMaturity"][0]),
 805                        "| Coupons yield (average coupon daily yield * 365):           | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])),
 806                        "| Current price yield (average daily yield * 365):            | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])),
 807                    ])
 808
 809                if "aciValue" in iJSON.keys() and iJSON["aciValue"]:
 810                    info.append("| Current accumulated coupon income (ACI):                    | {:<54} |\n".format("{:.2f} {}".format(
 811                        NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]),
 812                        iJSON["aciValue"]["currency"]
 813                    )))
 814
 815            if "currentPrice" in iJSON.keys():
 816                info.append(splitLine)
 817
 818                currency = iJSON["currency"] if "currency" in iJSON.keys() else ""  # nominal currency for bonds, otherwise currency of instrument
 819                aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else ""  # payment currency
 820
 821                bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0  # previous close price of bond
 822                bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0  # last price of bond
 823                bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0  # max price of bond
 824                bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0  # min price of bond
 825                bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0  # delta between last deal price and last close
 826
 827                curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0
 828                curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0
 829
 830                info.extend([
 831                    "| Previous close price of the instrument:                     | {:<54} |\n".format("{}{}".format(
 832                        "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A",
 833                        "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
 834                    )),
 835                    "| Last deal price of the instrument:                          | {:<54} |\n".format("{}{}".format(
 836                        "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A",
 837                        "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
 838                    )),
 839                    "| Changes between last deal price and last close              | {:<54} |\n".format(
 840                        "{:.2f}%{}".format(
 841                            iJSON["currentPrice"]["changes"],
 842                            " ({}{:.2f} {})".format(
 843                                "+" if bondChangesDelta > 0 else "",
 844                                bondChangesDelta,
 845                                aciCurrency
 846                            ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format(
 847                                "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "",
 848                                iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"],
 849                                currency
 850                            ),
 851                        )
 852                    ),
 853                    "| Current limit price, min / max:                             | {:<54} |\n".format("{}{} / {}{}{}".format(
 854                        "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A",
 855                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 856                        "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A",
 857                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 858                        " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else ""
 859                    )),
 860                    "| Actual price, sell / buy:                                   | {:<54} |\n".format("{}{} / {}{}{}".format(
 861                        "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A",
 862                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 863                        "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A",
 864                        "%" if iJSON["type"] == "Bonds" else" {}".format(currency),
 865                        " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else ""
 866                    )),
 867                ])
 868
 869            if "lot" in iJSON.keys():
 870                info.append("| Minimum lot to buy:                                         | {:<54} |\n".format(iJSON["lot"]))
 871
 872            if "step" in iJSON.keys() and iJSON["step"] != 0:
 873                info.append("| Minimum price increment (step):                             | {:<54} |\n".format("{} {}".format(iJSON["step"], iJSON["currency"] if "currency" in iJSON.keys() else "")))
 874
 875            # Add bond payment calendar:
 876            if iJSON["type"] == "Bonds":
 877                strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False)   # bond payment calendar
 878                info.extend(["\n#", strCalendar])
 879
 880            infoText += "".join(info)
 881
 882            if show:
 883                uLogger.info("{}".format(infoText))
 884
 885            else:
 886                uLogger.debug("{}".format(infoText))
 887
 888            if self.infoFile is not None:
 889                with open(self.infoFile, "w", encoding="UTF-8") as fH:
 890                    fH.write(infoText)
 891
 892                uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile)))
 893
 894                if self.useHTMLReports:
 895                    htmlFilePath = self.infoFile.replace(".md", ".html") if self.infoFile.endswith(".md") else self.infoFile + ".html"
 896                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
 897                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Main information", commonCSS=COMMON_CSS, markdown=infoText))
 898
 899                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
 900
 901        return infoText
 902
 903    def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict:
 904        """
 905        Search and return raw broker's information about instrument by its ticker. Variable `ticker` must be defined!
 906
 907        :param requestPrice: if `False` then do not request current price of instrument (because this is long operation).
 908        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
 909        :return: JSON formatted data with information about instrument.
 910        """
 911        tickerJSON = {}
 912        if self.moreDebug:
 913            uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self._ticker))
 914
 915        if not self._ticker:
 916            uLogger.warning("self._ticker variable is not be empty!")
 917
 918        else:
 919            if self._ticker in TKS_TICKERS_OR_FIGI_EXCLUDED:
 920                uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self._ticker))
 921                raise Exception("Instrument not allowed")
 922
 923            if not self.iList:
 924                self.iList = self.Listing()
 925
 926            if self._ticker in self.iList["Shares"].keys():
 927                tickerJSON = self.iList["Shares"][self._ticker]
 928                if self.moreDebug:
 929                    uLogger.debug("Ticker [{}] found in shares list".format(self._ticker))
 930
 931            elif self._ticker in self.iList["Currencies"].keys():
 932                tickerJSON = self.iList["Currencies"][self._ticker]
 933                if self.moreDebug:
 934                    uLogger.debug("Ticker [{}] found in currencies list".format(self._ticker))
 935
 936            elif self._ticker in self.iList["Bonds"].keys():
 937                tickerJSON = self.iList["Bonds"][self._ticker]
 938                if self.moreDebug:
 939                    uLogger.debug("Ticker [{}] found in bonds list".format(self._ticker))
 940
 941            elif self._ticker in self.iList["Etfs"].keys():
 942                tickerJSON = self.iList["Etfs"][self._ticker]
 943                if self.moreDebug:
 944                    uLogger.debug("Ticker [{}] found in etfs list".format(self._ticker))
 945
 946            elif self._ticker in self.iList["Futures"].keys():
 947                tickerJSON = self.iList["Futures"][self._ticker]
 948                if self.moreDebug:
 949                    uLogger.debug("Ticker [{}] found in futures list".format(self._ticker))
 950
 951        if tickerJSON:
 952            self._figi = tickerJSON["figi"]
 953
 954            if requestPrice:
 955                tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False)
 956
 957                if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None:
 958                    tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"]
 959
 960                else:
 961                    tickerJSON["currentPrice"]["changes"] = 0
 962
 963            if show:
 964                self.ShowInstrumentInfo(iJSON=tickerJSON, show=True)  # print info as Markdown text
 965
 966        else:
 967            if show:
 968                uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self._ticker))
 969
 970        return tickerJSON
 971
 972    def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict:
 973        """
 974        Search and return raw broker's information about instrument by its FIGI. Variable `figi` must be defined!
 975
 976        :param requestPrice: if `False` then do not request current price of instrument (it's long operation).
 977        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
 978        :return: JSON formatted data with information about instrument.
 979        """
 980        figiJSON = {}
 981        if self.moreDebug:
 982            uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self._figi))
 983
 984        if not self._figi:
 985            uLogger.warning("self._figi variable is not be empty!")
 986
 987        else:
 988            if self._figi in TKS_TICKERS_OR_FIGI_EXCLUDED:
 989                uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self._figi))
 990                raise Exception("Instrument not allowed")
 991
 992            if not self.iList:
 993                self.iList = self.Listing()
 994
 995            for item in self.iList["Shares"].keys():
 996                if self._figi == self.iList["Shares"][item]["figi"]:
 997                    figiJSON = self.iList["Shares"][item]
 998
 999                    if self.moreDebug:
1000                        uLogger.debug("FIGI [{}] found in shares list".format(self._figi))
1001
1002                    break
1003
1004            if not figiJSON:
1005                for item in self.iList["Currencies"].keys():
1006                    if self._figi == self.iList["Currencies"][item]["figi"]:
1007                        figiJSON = self.iList["Currencies"][item]
1008
1009                        if self.moreDebug:
1010                            uLogger.debug("FIGI [{}] found in currencies list".format(self._figi))
1011
1012                        break
1013
1014            if not figiJSON:
1015                for item in self.iList["Bonds"].keys():
1016                    if self._figi == self.iList["Bonds"][item]["figi"]:
1017                        figiJSON = self.iList["Bonds"][item]
1018
1019                        if self.moreDebug:
1020                            uLogger.debug("FIGI [{}] found in bonds list".format(self._figi))
1021
1022                        break
1023
1024            if not figiJSON:
1025                for item in self.iList["Etfs"].keys():
1026                    if self._figi == self.iList["Etfs"][item]["figi"]:
1027                        figiJSON = self.iList["Etfs"][item]
1028
1029                        if self.moreDebug:
1030                            uLogger.debug("FIGI [{}] found in etfs list".format(self._figi))
1031
1032                        break
1033
1034            if not figiJSON:
1035                for item in self.iList["Futures"].keys():
1036                    if self._figi == self.iList["Futures"][item]["figi"]:
1037                        figiJSON = self.iList["Futures"][item]
1038
1039                        if self.moreDebug:
1040                            uLogger.debug("FIGI [{}] found in futures list".format(self._figi))
1041
1042                        break
1043
1044        if figiJSON:
1045            self._figi = figiJSON["figi"]
1046            self._ticker = figiJSON["ticker"]
1047
1048            if requestPrice:
1049                figiJSON["currentPrice"] = self.GetCurrentPrices(show=False)
1050
1051                if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None:
1052                    figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"]
1053
1054                else:
1055                    figiJSON["currentPrice"]["changes"] = 0
1056
1057            if show:
1058                self.ShowInstrumentInfo(iJSON=figiJSON, show=True)  # print info as Markdown text
1059
1060        else:
1061            if show:
1062                uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self._figi))
1063
1064        return figiJSON
1065
1066    def GetCurrentPrices(self, show: bool = True) -> dict:
1067        """
1068        Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5:
1069        `{"buy": [{"price": 1243.8, "quantity": 193},
1070                  {"price": 1244.0, "quantity": 168},
1071                  {"price": 1244.8, "quantity": 5},
1072                  {"price": 1245.0, "quantity": 61},
1073                  {"price": 1245.4, "quantity": 60}],
1074          "sell": [{"price": 1243.6, "quantity": 8},
1075                   {"price": 1242.6, "quantity": 10},
1076                   {"price": 1242.4, "quantity": 18},
1077                   {"price": 1242.2, "quantity": 50},
1078                   {"price": 1242.0, "quantity": 113}],
1079          "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean:
1080        - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order
1081        - sell: list of dicts with Buyers prices,
1082            - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument),
1083            - quantity: volume value by current price in lots,
1084        - limitUp: current trade session limit price, maximum,
1085        - limitDown: current trade session limit price, minimum,
1086        - lastPrice: last deal price of the instrument,
1087        - closePrice: previous trade session close price of the instrument.
1088
1089        See also: `SearchByTicker()` and `SearchByFIGI()`.
1090        REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1091        Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1092
1093        :param show: if `True` then print DOM to log and console.
1094        :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`.
1095                 If an error occurred then returns an empty record:
1096                 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`.
1097        """
1098        prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0}
1099
1100        if self.depth < 1:
1101            uLogger.error("Depth of Market (DOM) must be >=1!")
1102            raise Exception("Incorrect value")
1103
1104        if not (self._ticker or self._figi):
1105            uLogger.error("self._ticker or self._figi variables must be defined!")
1106            raise Exception("Ticker or FIGI required")
1107
1108        if self._ticker and not self._figi:
1109            instrumentByTicker = self.SearchByTicker(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1110            self._figi = instrumentByTicker["figi"] if instrumentByTicker else ""
1111
1112        if not self._ticker and self._figi:
1113            instrumentByFigi = self.SearchByFIGI(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1114            self._ticker = instrumentByFigi["ticker"] if instrumentByFigi else ""
1115
1116        if not self._figi:
1117            uLogger.error("FIGI is not defined!")
1118            raise Exception("Ticker or FIGI required")
1119
1120        else:
1121            uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self._ticker, self._figi))
1122
1123            # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1124            priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook"
1125            self.body = str({"figi": self._figi, "depth": self.depth})
1126            pricesResponse = self.SendAPIRequest(priceURL, reqType="POST")  # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1127
1128            if pricesResponse and not ("code" in pricesResponse.keys() or "message" in pricesResponse.keys() or "description" in pricesResponse.keys()):
1129                # list of dicts with sellers orders:
1130                prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]]
1131
1132                # list of dicts with buyers orders:
1133                prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]]
1134
1135                # max price of instrument at this time:
1136                prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None
1137
1138                # min price of instrument at this time:
1139                prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None
1140
1141                # last price of deal with instrument:
1142                prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0
1143
1144                # last close price of instrument:
1145                prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0
1146
1147            else:
1148                uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi))
1149                uLogger.debug("Server response: {}".format(pricesResponse))
1150
1151            if show:
1152                if prices["buy"] or prices["sell"]:
1153                    info = [
1154                        "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format(
1155                            datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
1156                            self._ticker,
1157                            self._figi,
1158                            self.depth,
1159                        ),
1160                        "-" * 60, "\n",
1161                        "             Orders of Buyers | Orders of Sellers\n",
1162                        "-" * 60, "\n",
1163                        "        Sell prices (volumes) | Buy prices (volumes)\n",
1164                        "-" * 60, "\n",
1165                    ]
1166
1167                    if not prices["buy"]:
1168                        info.append("                              | No orders!\n")
1169                        sumBuy = 0
1170
1171                    else:
1172                        sumBuy = sum([x["quantity"] for x in prices["buy"]])
1173                        maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True)
1174                        for item in maxMinSorted:
1175                            info.append("                              | {} ({})\n".format(item["price"], item["quantity"]))
1176
1177                    if not prices["sell"]:
1178                        info.append("No orders!                    |\n")
1179                        sumSell = 0
1180
1181                    else:
1182                        sumSell = sum([x["quantity"] for x in prices["sell"]])
1183                        for item in prices["sell"]:
1184                            info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"])))
1185
1186                    info.extend([
1187                        "-" * 60, "\n",
1188                        "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)),
1189                        "-" * 60, "\n",
1190                    ])
1191
1192                    infoText = "".join(info)
1193
1194                    uLogger.info("Current prices in order book:\n\n{}".format(infoText))
1195
1196                else:
1197                    uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi))
1198
1199        return prices
1200
1201    def ShowInstrumentsInfo(self, show: bool = True) -> str:
1202        """
1203        This method get and show information about all available broker instruments for current user account.
1204        If `instrumentsFile` string is not empty then also save information to this file.
1205
1206        :param show: if `True` then print results to console, if `False` — print only to file.
1207        :return: multi-lines string with all available broker instruments
1208        """
1209        if not self.iList:
1210            self.iList = self.Listing()
1211
1212        info = [
1213            "# All available instruments from Tinkoff Broker server for current user token\n\n",
1214            "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
1215        ]
1216
1217        # add instruments count by type:
1218        for iType in self.iList.keys():
1219            info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType])))
1220
1221        headerLine = "| Ticker       | Full name                                                 | FIGI         | Cur | Lot     | Step       |\n"
1222        splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n"
1223
1224        # generating info tables with all instruments by type:
1225        for iType in self.iList.keys():
1226            info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine])
1227
1228            for instrument in self.iList[iType].keys():
1229                iName = self.iList[iType][instrument]["name"]  # instrument's name
1230                if len(iName) > 57:
1231                    iName = "{}...".format(iName[:54])  # right trim for a long string
1232
1233                info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format(
1234                    self.iList[iType][instrument]["ticker"],
1235                    iName,
1236                    self.iList[iType][instrument]["figi"],
1237                    self.iList[iType][instrument]["currency"],
1238                    self.iList[iType][instrument]["lot"],
1239                    "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0,
1240                ))
1241
1242        infoText = "".join(info)
1243
1244        if show:
1245            uLogger.info(infoText)
1246
1247        if self.instrumentsFile:
1248            with open(self.instrumentsFile, "w", encoding="UTF-8") as fH:
1249                fH.write(infoText)
1250
1251            uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile)))
1252
1253            if self.useHTMLReports:
1254                htmlFilePath = self.instrumentsFile.replace(".md", ".html") if self.instrumentsFile.endswith(".md") else self.instrumentsFile + ".html"
1255                with open(htmlFilePath, "w", encoding="UTF-8") as fH:
1256                    fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="List of instruments", commonCSS=COMMON_CSS, markdown=infoText))
1257
1258                uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
1259
1260        return infoText
1261
1262    def SearchInstruments(self, pattern: str, show: bool = True) -> dict:
1263        """
1264        This method search and show information about instruments by part of its ticker, FIGI or name.
1265        If `searchResultsFile` string is not empty then also save information to this file.
1266
1267        :param pattern: string with part of ticker, FIGI or instrument's name.
1268        :param show: if `True` then print results to console, if `False` — return list of result only.
1269        :return: list of dictionaries with all found instruments.
1270        """
1271        if not self.iList:
1272            self.iList = self.Listing()
1273
1274        searchResults = {iType: {} for iType in self.iList}  # same as iList but will contain only filtered instruments
1275        compiledPattern = re.compile(pattern, re.IGNORECASE)
1276
1277        for iType in self.iList:
1278            for instrument in self.iList[iType].values():
1279                searchResult = compiledPattern.search(" ".join(
1280                    [instrument["ticker"], instrument["figi"], instrument["name"]]
1281                ))
1282
1283                if searchResult:
1284                    searchResults[iType][instrument["ticker"]] = instrument
1285
1286        resultsLen = sum([len(searchResults[iType]) for iType in searchResults])
1287        info = [
1288            "# Search results\n\n",
1289            "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
1290            "* **Search pattern:** [{}]\n".format(pattern),
1291            "* **Found instruments:** [{}]\n\n".format(resultsLen),
1292            '**Note:** you can view info about found instruments with key "--info", e.g.: "tksbrokerapi -t TICKER --info" or "tksbrokerapi -f FIGI --info".\n'
1293        ]
1294        infoShort = info[:]
1295
1296        headerLine = "| Type       | Ticker       | Full name                                                      | FIGI         |\n"
1297        splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n"
1298        skippedLine = "| ...        | ...          | ...                                                            | ...          |\n"
1299
1300        if resultsLen == 0:
1301            info.append("\nNo results\n")
1302            infoShort.append("\nNo results\n")
1303            uLogger.warning("No results. Try changing your search pattern.")
1304
1305        else:
1306            for iType in searchResults:
1307                iTypeValuesCount = len(searchResults[iType].values())
1308                if iTypeValuesCount > 0:
1309                    info.extend(["\n## {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1310                    infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1311
1312                    for instrument in searchResults[iType].values():
1313                        info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format(
1314                            instrument["type"],
1315                            instrument["ticker"],
1316                            "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"],  # right trim for a long string
1317                            instrument["figi"],
1318                        ))
1319
1320                    if iTypeValuesCount <= 5:
1321                        infoShort.extend(info[-iTypeValuesCount:])
1322
1323                    else:
1324                        infoShort.extend(info[-5:])
1325                        infoShort.append(skippedLine)
1326
1327        infoText = "".join(info)
1328        infoTextShort = "".join(infoShort)
1329
1330        if show:
1331            uLogger.info(infoTextShort)
1332            uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`")
1333
1334        if self.searchResultsFile:
1335            with open(self.searchResultsFile, "w", encoding="UTF-8") as fH:
1336                fH.write(infoText)
1337
1338            uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile)))
1339
1340            if self.useHTMLReports:
1341                htmlFilePath = self.searchResultsFile.replace(".md", ".html") if self.searchResultsFile.endswith(".md") else self.searchResultsFile + ".html"
1342                with open(htmlFilePath, "w", encoding="UTF-8") as fH:
1343                    fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Search results", commonCSS=COMMON_CSS, markdown=infoText))
1344
1345                uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
1346
1347        return searchResults
1348
1349    def GetUniqueFIGIs(self, instruments: list[str]) -> list:
1350        """
1351        Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs.
1352
1353        :param instruments: list of strings with tickers or FIGIs.
1354        :return: list with unique instrument FIGIs only.
1355        """
1356        requestedInstruments = []
1357        for iName in instruments:
1358            if iName not in self.aliases.keys():
1359                if iName not in requestedInstruments:
1360                    requestedInstruments.append(iName)
1361
1362            else:
1363                if iName not in requestedInstruments:
1364                    if self.aliases[iName] not in requestedInstruments:
1365                        requestedInstruments.append(self.aliases[iName])
1366
1367        uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments))
1368
1369        onlyUniqueFIGIs = []
1370        for iName in requestedInstruments:
1371            if iName in TKS_TICKERS_OR_FIGI_EXCLUDED:
1372                continue
1373
1374            self._ticker = iName
1375            iData = self.SearchByTicker(requestPrice=False)  # trying to find instrument by ticker
1376
1377            if not iData:
1378                self._ticker = ""
1379                self._figi = iName
1380
1381                iData = self.SearchByFIGI(requestPrice=False)  # trying to find instrument by FIGI
1382
1383                if not iData:
1384                    self._figi = ""
1385                    uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName))
1386
1387            if iData and iData["figi"] not in onlyUniqueFIGIs:
1388                onlyUniqueFIGIs.append(iData["figi"])
1389
1390        uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs))
1391
1392        return onlyUniqueFIGIs
1393
1394    def GetListOfPrices(self, instruments: list[str], show: bool = False) -> list[dict]:
1395        """
1396        This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation!
1397
1398        See limits: https://tinkoff.github.io/investAPI/limits/
1399
1400        If `pricesFile` string is not empty then also save information to this file.
1401
1402        :param instruments: list of strings with tickers or FIGIs.
1403        :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`.
1404        :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1405                 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods.
1406        """
1407        if instruments is None or not instruments:
1408            uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!")
1409            raise Exception("Ticker or FIGI required")
1410
1411        onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments)
1412
1413        uLogger.debug("Requesting current prices from Tinkoff Broker server...")
1414
1415        iList = []  # trying to get info and current prices about all unique instruments:
1416        for self._figi in onlyUniqueFIGIs:
1417            iData = self.SearchByFIGI(requestPrice=True)
1418            iList.append(iData)
1419
1420        self.ShowListOfPrices(iList, show)
1421
1422        return iList
1423
1424    def ShowListOfPrices(self, iList: list, show: bool = True) -> str:
1425        """
1426        Show table contains current prices of given instruments.
1427
1428        :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1429                      One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods.
1430        :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`.
1431        :return: multilines text in Markdown format as a table contains current prices.
1432        """
1433        infoText = ""
1434
1435        if show or self.pricesFile:
1436            info = [
1437                "# Current prices\n\n* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
1438                "| Ticker       | FIGI         | Type       | Prev. close | Last price  | Chg. %   | Day limits min/max  | Actual sell / buy   | Curr. |\n",
1439                "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n",
1440            ]
1441
1442            for item in iList:
1443                info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format(
1444                    item["ticker"],
1445                    item["figi"],
1446                    item["type"],
1447                    "{:.2f}".format(float(item["currentPrice"]["closePrice"])),
1448                    "{:.2f}".format(float(item["currentPrice"]["lastPrice"])),
1449                    "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])),
1450                    "{} / {}".format(
1451                        item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A",
1452                        item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A",
1453                    ),
1454                    "{} / {}".format(
1455                        item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A",
1456                        item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A",
1457                    ),
1458                    item["currency"],
1459                ))
1460
1461            infoText = "".join(info)
1462
1463            if show:
1464                uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText))
1465
1466            if self.pricesFile:
1467                with open(self.pricesFile, "w", encoding="UTF-8") as fH:
1468                    fH.write(infoText)
1469
1470                uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile)))
1471
1472                if self.useHTMLReports:
1473                    htmlFilePath = self.pricesFile.replace(".md", ".html") if self.pricesFile.endswith(".md") else self.pricesFile + ".html"
1474                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
1475                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Current prices", commonCSS=COMMON_CSS, markdown=infoText))
1476
1477                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
1478
1479        return infoText
1480
1481    def RequestTradingStatus(self) -> dict:
1482        """
1483        Requesting trading status for the instrument defined by `figi` variable.
1484
1485        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus
1486
1487        Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest
1488
1489        :return: dictionary with trading status attributes. Response example:
1490                 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING",
1491                  "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}`
1492        """
1493        if self._figi is None or not self._figi:
1494            uLogger.error("Variable `figi` must be defined for using this method!")
1495            raise Exception("FIGI required")
1496
1497        uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self._figi))
1498
1499        self.body = str({"figi": self._figi, "instrumentId": self._figi})
1500        tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus"
1501        tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST")
1502
1503        if self.moreDebug:
1504            uLogger.debug("Records about current trading status successfully received")
1505
1506        return tradingStatus
1507
1508    def RequestPortfolio(self) -> dict:
1509        """
1510        Requesting actual user's portfolio for current `accountId`.
1511
1512        REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio
1513
1514        Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest
1515
1516        :return: dictionary with user's portfolio.
1517        """
1518        if self.accountId is None or not self.accountId:
1519            uLogger.error("Variable `accountId` must be defined for using this method!")
1520            raise Exception("Account ID required")
1521
1522        uLogger.debug("Requesting current actual user's portfolio. Wait, please...")
1523
1524        self.body = str({"accountId": self.accountId})
1525        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio"
1526        rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST")
1527
1528        if self.moreDebug:
1529            uLogger.debug("Records about user's portfolio successfully received")
1530
1531        return rawPortfolio
1532
1533    def RequestPositions(self) -> dict:
1534        """
1535        Requesting open positions by currencies and instruments for current `accountId`.
1536
1537        REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions
1538
1539        Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest
1540
1541        :return: dictionary with open positions by instruments.
1542        """
1543        if self.accountId is None or not self.accountId:
1544            uLogger.error("Variable `accountId` must be defined for using this method!")
1545            raise Exception("Account ID required")
1546
1547        uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...")
1548
1549        self.body = str({"accountId": self.accountId})
1550        positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions"
1551        rawPositions = self.SendAPIRequest(positionsURL, reqType="POST")
1552
1553        if self.moreDebug:
1554            uLogger.debug("Records about current open positions successfully received")
1555
1556        return rawPositions
1557
1558    def RequestPendingOrders(self) -> list:
1559        """
1560        Requesting current actual pending limit orders for current `accountId`.
1561
1562        REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders
1563
1564        Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest
1565
1566        :return: list of dictionaries with pending limit orders.
1567        """
1568        if self.accountId is None or not self.accountId:
1569            uLogger.error("Variable `accountId` must be defined for using this method!")
1570            raise Exception("Account ID required")
1571
1572        uLogger.debug("Requesting current actual pending limit orders. Wait, please...")
1573
1574        self.body = str({"accountId": self.accountId})
1575        ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders"
1576        rawResponse = self.SendAPIRequest(ordersURL, reqType="POST")
1577
1578        if "orders" in rawResponse.keys():
1579            rawOrders = rawResponse["orders"]
1580            uLogger.debug("[{}] records about pending limit orders received".format(len(rawOrders)))
1581
1582        else:
1583            rawOrders = []
1584            uLogger.debug("No pending limit orders returned! rawResponse = {}".format(rawResponse))
1585
1586        return rawOrders
1587
1588    def RequestStopOrders(self) -> list:
1589        """
1590        Requesting current actual stop orders for current `accountId`.
1591
1592        REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders
1593
1594        Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest
1595
1596        :return: list of dictionaries with stop orders.
1597        """
1598        if self.accountId is None or not self.accountId:
1599            uLogger.error("Variable `accountId` must be defined for using this method!")
1600            raise Exception("Account ID required")
1601
1602        uLogger.debug("Requesting current actual stop orders. Wait, please...")
1603
1604        self.body = str({"accountId": self.accountId})
1605        stopOrdersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders"
1606        rawResponse = self.SendAPIRequest(stopOrdersURL, reqType="POST")
1607
1608        if "stopOrders" in rawResponse.keys():
1609            rawStopOrders = rawResponse["stopOrders"]
1610            uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders)))
1611
1612        else:
1613            rawStopOrders = []
1614            uLogger.debug("No stop orders returned! rawResponse = {}".format(rawResponse))
1615
1616        return rawStopOrders
1617
1618    def Overview(self, show: bool = False, details: str = "full") -> dict:
1619        """
1620        Get portfolio: all open positions, orders and some statistics for current `accountId`.
1621        If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile`
1622        and `overviewBondsCalendarFile` are defined then also save information to file.
1623
1624        WARNING! It is not recommended to run this method too many times in a loop! The server receives
1625        many requests about the state of the portfolio, and then, based on the received data, a large number
1626        of calculation and statistics are collected.
1627
1628        :param show: if `False` then only dictionary returns, if `True` then show more debug information.
1629        :param details: how detailed should the information be?
1630        - `full` — shows full available information about portfolio status (by default),
1631        - `positions` — shows only open positions,
1632        - `orders` — shows only sections of open limits and stop orders.
1633        - `digest` — show a short digest of the portfolio status,
1634        - `analytics` — shows only the analytics section and the distribution of the portfolio by various categories,
1635        - `calendar` — shows only the bonds calendar section (if these present in portfolio),
1636        :return: dictionary with client's raw portfolio and some statistics.
1637        """
1638        if self.accountId is None or not self.accountId:
1639            uLogger.error("Variable `accountId` must be defined for using this method!")
1640            raise Exception("Account ID required")
1641
1642        view = {
1643            "raw": {  # --- raw portfolio responses from broker with user portfolio data:
1644                "headers": {},  # list of dictionaries, response headers without "positions" section
1645                "Currencies": [],  # list of dictionaries, open trades with currencies from "positions" section
1646                "Shares": [],  # list of dictionaries, open trades with shares from "positions" section
1647                "Bonds": [],  # list of dictionaries, open trades with bonds from "positions" section
1648                "Etfs": [],  # list of dictionaries, open trades with etfs from "positions" section
1649                "Futures": [],  # list of dictionaries, open trades with futures from "positions" section
1650                "positions": {},  # raw response from broker: dictionary with current available or blocked currencies and instruments for client
1651                "orders": [],  # raw response from broker: list of dictionaries with all pending (market) orders
1652                "stopOrders": [],  # raw response from broker: list of dictionaries with all stop orders
1653                "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}},  # dict with prices of all currencies in RUB
1654            },
1655            "stat": {  # --- some statistics calculated using "raw" sections:
1656                "portfolioCostRUB": 0.,  # portfolio cost in RUB (Russian Rouble)
1657                "availableRUB": 0.,  # available rubles (without other currencies)
1658                "blockedRUB": 0.,  # blocked sum in Russian Rouble
1659                "totalChangesRUB": 0.,  # changes for all open trades in RUB
1660                "totalChangesPercentRUB": 0.,  # changes for all open trades in percents
1661                "allCurrenciesCostRUB": 0.,  # costs of all currencies (include rubles) in RUB
1662                "sharesCostRUB": 0.,  # costs of all shares in RUB
1663                "bondsCostRUB": 0.,  # costs of all bonds in RUB
1664                "etfsCostRUB": 0.,  # costs of all etfs in RUB
1665                "futuresCostRUB": 0.,  # costs of all futures in RUB
1666                "Currencies": [],  # list of dictionaries of all currencies statistics
1667                "Shares": [],  # list of dictionaries of all shares statistics
1668                "Bonds": [],  # list of dictionaries of all bonds statistics
1669                "Etfs": [],  # list of dictionaries of all etfs statistics
1670                "Futures": [],  # list of dictionaries of all futures statistics
1671                "orders": [],  # list of dictionaries of all pending (market) orders and it's parameters
1672                "stopOrders": [],  # list of dictionaries of all stop orders and it's parameters
1673                "blockedCurrencies": {},  # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21}
1674                "blockedInstruments": {},  # dict with blocked  by FIGI, e.g. {}
1675                "funds": {},  # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1676            },
1677            "analytics": {  # --- some analytics of portfolio:
1678                "distrByAssets": {},  # portfolio distribution by assets
1679                "distrByCompanies": {},  # portfolio distribution by companies
1680                "distrBySectors": {},  # portfolio distribution by sectors
1681                "distrByCurrencies": {},  # portfolio distribution by currencies
1682                "distrByCountries": {},  # portfolio distribution by countries
1683                "bondsCalendar": None,  # bonds payment calendar as Pandas DataFrame (if these present in portfolio)
1684            }
1685        }
1686
1687        details = details.lower()
1688        availableDetails = ["full", "positions", "orders", "analytics", "calendar", "digest"]
1689        if details not in availableDetails:
1690            details = "full"
1691            uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails))
1692
1693        uLogger.debug("Requesting portfolio of a client. Wait, please...")
1694
1695        portfolioResponse = self.RequestPortfolio()  # current user's portfolio (dict)
1696        view["raw"]["positions"] = self.RequestPositions()  # current open positions by instruments (dict)
1697        view["raw"]["orders"] = self.RequestPendingOrders()  # current actual pending limit orders (list)
1698        view["raw"]["stopOrders"] = self.RequestStopOrders()  # current actual stop orders (list)
1699
1700        # save response headers without "positions" section:
1701        for key in portfolioResponse.keys():
1702            if key != "positions":
1703                view["raw"]["headers"][key] = portfolioResponse[key]
1704
1705            else:
1706                continue
1707
1708        # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation
1709        # Type of instrument must be only one of supported types in TKS_INSTRUMENTS
1710        for item in portfolioResponse["positions"]:
1711            if item["instrumentType"] == "currency":
1712                self._figi = item["figi"]
1713                if not self._figi and item["ticker"]:
1714                    self._ticker = item["ticker"]
1715                    self._figi = self.SearchByTicker()["figi"]  # Get FIGI to avoid warnings
1716
1717                curr = self.SearchByFIGI(requestPrice=False)
1718
1719                # current price of currency in RUB:
1720                view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = {
1721                    "name": curr["name"],
1722                    "currentPrice": NanoToFloat(
1723                        item["currentPrice"]["units"],
1724                        item["currentPrice"]["nano"]
1725                    ),
1726                }
1727
1728                view["raw"]["Currencies"].append(item)
1729
1730            elif item["instrumentType"] == "share":
1731                view["raw"]["Shares"].append(item)
1732
1733            elif item["instrumentType"] == "bond":
1734                view["raw"]["Bonds"].append(item)
1735
1736            elif item["instrumentType"] == "etf":
1737                view["raw"]["Etfs"].append(item)
1738
1739            elif item["instrumentType"] == "futures":
1740                view["raw"]["Futures"].append(item)
1741
1742            else:
1743                continue
1744
1745        # how many volume of currencies (by ISO currency name) are blocked:
1746        for item in view["raw"]["positions"]["blocked"]:
1747            blocked = NanoToFloat(item["units"], item["nano"])
1748            if blocked > 0:
1749                view["stat"]["blockedCurrencies"][item["currency"]] = blocked
1750
1751        # how many volume of instruments (by FIGI) are blocked:
1752        for item in view["raw"]["positions"]["securities"]:
1753            blocked = int(item["blocked"])
1754            if blocked > 0:
1755                view["stat"]["blockedInstruments"][item["figi"]] = blocked
1756
1757        allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]}
1758
1759        if "rub" in allBlocked.keys():
1760            view["stat"]["blockedRUB"] = allBlocked["rub"]  # blocked rubles
1761
1762        # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies:
1763        view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"])
1764        view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"])
1765        view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"])
1766        view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"])
1767        view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"])
1768        view["stat"]["portfolioCostRUB"] = sum([
1769            view["stat"]["allCurrenciesCostRUB"],
1770            view["stat"]["sharesCostRUB"],
1771            view["stat"]["bondsCostRUB"],
1772            view["stat"]["etfsCostRUB"],
1773            view["stat"]["futuresCostRUB"],
1774        ])
1775
1776        # --- calculating some portfolio statistics:
1777        byComp = {}  # distribution by companies
1778        bySect = {}  # distribution by sectors
1779        byCurr = {}  # distribution by currencies (include RUB)
1780        unknownCountryName = "All other countries"  # default name for instruments without "countryOfRisk" and "countryOfRiskName"
1781        byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}}  # distribution by countries (currencies are included in their countries)
1782
1783        for item in portfolioResponse["positions"]:
1784            self._figi = item["figi"]
1785            if not self._figi and item["ticker"]:
1786                self._ticker = item["ticker"]
1787                self._figi = self.SearchByTicker()["figi"]  # Get FIGI to avoid warnings
1788
1789            instrument = self.SearchByFIGI(requestPrice=False)  # full raw info about instrument by FIGI
1790
1791            if instrument:
1792                if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys():
1793                    blocked = allBlocked[instrument["nominal"]["currency"]]  # blocked volume of currency
1794
1795                elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys():
1796                    blocked = allBlocked[item["figi"]]  # blocked volume of other instruments
1797
1798                else:
1799                    blocked = 0
1800
1801                volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"])  # available volume of instrument
1802                lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"])  # available volume in lots of instrument
1803                direction = "Long" if lots >= 0 else "Short"  # direction of an instrument's position: short or long
1804                curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"])  # current instrument's price
1805                average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"])  # current average position price
1806                profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"])  # expected profit at current moment
1807                currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"]  # currency name rub, usd, eur etc.
1808                cost = curPrice if "currentNkd" not in item.keys() else (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume  # current cost of all volume of instrument in basic asset
1809                baseCurrencyName = item["currentPrice"]["currency"]  # name of base currency (rub)
1810                countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName
1811                costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"]  # cost in rubles
1812                percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.  # instrument's part in percent of full portfolio cost
1813
1814                statData = {
1815                    "figi": item["figi"],  # FIGI from REST API "GetPortfolio" method
1816                    "ticker": instrument["ticker"],  # ticker by FIGI
1817                    "currency": currency,  # currency name rub, usd, eur etc. for instrument price
1818                    "volume": volume,  # available volume of instrument
1819                    "lots": lots,  # volume in lots of instrument
1820                    "direction": direction,  # direction of an instrument's position: short or long
1821                    "blocked": blocked,  # blocked volume of currency or instrument
1822                    "currentPrice": curPrice,  # current instrument's price in basic asset
1823                    "average": average,  # current average position price
1824                    "cost": cost,  # current cost of all volume of instrument in basic asset
1825                    "baseCurrencyName": baseCurrencyName,  # name of base currency (rub)
1826                    "costRUB": costRUB,  # cost of instrument in ruble
1827                    "percentCostRUB": percentCostRUB,  # instrument's part in percent of full portfolio cost in RUB
1828                    "profit": profit,  # expected profit at current moment
1829                    "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0,  # expected percents of profit at current moment for this instrument
1830                    "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other",
1831                    "name": instrument["name"] if "name" in instrument.keys() else "",  # human-readable names of instruments
1832                    "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "",  # ISO name for currencies only
1833                    "country": countryName,  # e.g. "[RU] Российская Федерация" or unknownCountryName
1834                    "step": instrument["step"],  # minimum price increment
1835                }
1836
1837                # adding distribution by unique countries:
1838                if statData["country"] not in byCountry.keys():
1839                    byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB}
1840
1841                else:
1842                    byCountry[statData["country"]]["cost"] += costRUB
1843                    byCountry[statData["country"]]["percent"] += percentCostRUB
1844
1845                if item["instrumentType"] != "currency":
1846                    # adding distribution by unique companies:
1847                    if statData["name"]:
1848                        if statData["name"] not in byComp.keys():
1849                            byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB}
1850
1851                        else:
1852                            byComp[statData["name"]]["cost"] += costRUB
1853                            byComp[statData["name"]]["percent"] += percentCostRUB
1854
1855                    # adding distribution by unique sectors:
1856                    if statData["sector"] not in bySect.keys():
1857                        bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB}
1858
1859                    else:
1860                        bySect[statData["sector"]]["cost"] += costRUB
1861                        bySect[statData["sector"]]["percent"] += percentCostRUB
1862
1863                # adding distribution by unique currencies:
1864                if currency not in byCurr.keys():
1865                    byCurr[currency] = {
1866                        "name": view["raw"]["currenciesCurrentPrices"][currency]["name"],
1867                        "cost": costRUB,
1868                        "percent": percentCostRUB
1869                    }
1870
1871                else:
1872                    byCurr[currency]["cost"] += costRUB
1873                    byCurr[currency]["percent"] += percentCostRUB
1874
1875                # saving statistics for every instrument:
1876                if item["instrumentType"] == "currency":
1877                    view["stat"]["Currencies"].append(statData)
1878
1879                    # update dict with free funds for trading (total - blocked) by currencies
1880                    # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1881                    view["stat"]["funds"][currency] = {
1882                        "total": volume,
1883                        "totalCostRUB": costRUB,  # total volume cost in rubles
1884                        "free": volume - blocked,
1885                        "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0,  # free volume cost in rubles
1886                    }
1887
1888                elif item["instrumentType"] == "share":
1889                    view["stat"]["Shares"].append(statData)
1890
1891                elif item["instrumentType"] == "bond":
1892                    view["stat"]["Bonds"].append(statData)
1893
1894                elif item["instrumentType"] == "etf":
1895                    view["stat"]["Etfs"].append(statData)
1896
1897                elif item["instrumentType"] == "Futures":
1898                    view["stat"]["Futures"].append(statData)
1899
1900                else:
1901                    continue
1902
1903        # total changes in Russian Ruble:
1904        view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]])  # available RUB without other currencies
1905        view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0.
1906        startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100)
1907        view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost
1908        view["stat"]["funds"]["rub"] = {
1909            "total": view["stat"]["availableRUB"],
1910            "totalCostRUB": view["stat"]["availableRUB"],
1911            "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1912            "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1913        }
1914
1915        # --- pending limit orders sector data:
1916        uniquePendingOrdersFIGIs = []  # unique FIGIs of pending limit orders to avoid many times price requests
1917        uniquePendingOrders = {}  # unique instruments with FIGIs as dictionary keys
1918
1919        for item in view["raw"]["orders"]:
1920            self._figi = item["figi"]
1921
1922            if item["figi"] not in uniquePendingOrdersFIGIs:
1923                instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI, price requests only one time
1924
1925                uniquePendingOrdersFIGIs.append(item["figi"])
1926                uniquePendingOrders[item["figi"]] = instrument
1927
1928            else:
1929                instrument = uniquePendingOrders[item["figi"]]
1930
1931            if instrument:
1932                action = TKS_ORDER_DIRECTIONS[item["direction"]]
1933                orderType = TKS_ORDER_TYPES[item["orderType"]]
1934                orderState = TKS_ORDER_STATES[item["executionReportStatus"]]
1935                orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1936
1937                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1938                if item["direction"] == "ORDER_DIRECTION_BUY":
1939                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1940
1941                else:
1942                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
1943
1944                # requested price for order execution:
1945                target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"])
1946
1947                # necessary changes in percent to reach target from current price:
1948                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
1949
1950                view["stat"]["orders"].append({
1951                    "orderID": item["orderId"],  # orderId number parameter of current order
1952                    "figi": item["figi"],  # FIGI identification
1953                    "ticker": instrument["ticker"],  # ticker name by FIGI
1954                    "lotsRequested": item["lotsRequested"],  # requested lots value
1955                    "lotsExecuted": item["lotsExecuted"],  # how many lots are executed
1956                    "currentPrice": lastPrice,  # current instrument's price for defined action
1957                    "targetPrice": target,  # requested price for order execution in base currency
1958                    "baseCurrencyName": item["initialSecurityPrice"]["currency"],  # name of base currency
1959                    "percentChanges": changes,  # changes in percent to target from current price
1960                    "currency": item["currency"],  # instrument's currency name
1961                    "action": action,  # sell / buy / Unknown from TKS_ORDER_DIRECTIONS
1962                    "type": orderType,  # type of order from TKS_ORDER_TYPES
1963                    "status": orderState,  # order status from TKS_ORDER_STATES
1964                    "date": orderDate,  # string with order date and time from UTC format (without nano seconds part)
1965                })
1966
1967        # --- stop orders sector data:
1968        uniqueStopOrdersFIGIs = []  # unique FIGIs of stop orders to avoid many times price requests
1969        uniqueStopOrders = {}  # unique instruments with FIGIs as dictionary keys
1970
1971        for item in view["raw"]["stopOrders"]:
1972            self._figi = item["figi"]
1973
1974            if item["figi"] not in uniqueStopOrdersFIGIs:
1975                instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI, price requests only one time
1976
1977                uniqueStopOrdersFIGIs.append(item["figi"])
1978                uniqueStopOrders[item["figi"]] = instrument
1979
1980            else:
1981                instrument = uniqueStopOrders[item["figi"]]
1982
1983            if instrument:
1984                action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]]
1985                orderType = TKS_STOP_ORDER_TYPES[item["orderType"]]
1986                createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1987
1988                # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order
1989                if "expirationTime" in item.keys():
1990                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"]
1991                    expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0]
1992
1993                else:
1994                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"]
1995                    expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"]
1996
1997                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1998                if item["direction"] == "STOP_ORDER_DIRECTION_BUY":
1999                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
2000
2001                else:
2002                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
2003
2004                # requested price when stop-order executed:
2005                target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"])
2006
2007                # price for limit-order, set up when stop-order executed:
2008                limit = NanoToFloat(item["price"]["units"], item["price"]["nano"])
2009
2010                # necessary changes in percent to reach target from current price:
2011                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
2012
2013                view["stat"]["stopOrders"].append({
2014                    "orderID": item["stopOrderId"],  # stopOrderId number parameter of current stop-order
2015                    "figi": item["figi"],  # FIGI identification
2016                    "ticker": instrument["ticker"],  # ticker name by FIGI
2017                    "lotsRequested": item["lotsRequested"],  # requested lots value
2018                    "currentPrice": lastPrice,  # current instrument's price for defined action
2019                    "targetPrice": target,  # requested price for stop-order execution in base currency
2020                    "limitPrice": limit,  # price for limit-order, set up when stop-order executed, 0 if market order
2021                    "baseCurrencyName": item["stopPrice"]["currency"],  # name of base currency
2022                    "percentChanges": changes,  # changes in percent to target from current price
2023                    "currency": item["currency"],  # instrument's currency name
2024                    "action": action,  # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS
2025                    "type": orderType,  # type of order from TKS_STOP_ORDER_TYPES
2026                    "expType": expType,  # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES
2027                    "createDate": createDate,  # string with created order date and time from UTC format (without nano seconds part)
2028                    "expDate": expDate,  # string with expiration order date and time from UTC format (without nano seconds part)
2029                })
2030
2031        # --- calculating data for analytics section:
2032        # portfolio distribution by assets:
2033        view["analytics"]["distrByAssets"] = {
2034            "Ruble": {
2035                "uniques": 1,
2036                "cost": view["stat"]["availableRUB"],
2037                "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2038            },
2039            "Currencies": {
2040                "uniques": len(view["stat"]["Currencies"]),  # all foreign currencies without RUB
2041                "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"],
2042                "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2043            },
2044            "Shares": {
2045                "uniques": len(view["stat"]["Shares"]),
2046                "cost": view["stat"]["sharesCostRUB"],
2047                "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2048            },
2049            "Bonds": {
2050                "uniques": len(view["stat"]["Bonds"]),
2051                "cost": view["stat"]["bondsCostRUB"],
2052                "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2053            },
2054            "Etfs": {
2055                "uniques": len(view["stat"]["Etfs"]),
2056                "cost": view["stat"]["etfsCostRUB"],
2057                "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2058            },
2059            "Futures": {
2060                "uniques": len(view["stat"]["Futures"]),
2061                "cost": view["stat"]["futuresCostRUB"],
2062                "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2063            },
2064        }
2065
2066        # portfolio distribution by companies:
2067        view["analytics"]["distrByCompanies"]["All money cash"] = {
2068            "ticker": "",
2069            "cost": view["stat"]["allCurrenciesCostRUB"],
2070            "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2071        }
2072        view["analytics"]["distrByCompanies"].update(byComp)
2073
2074        # portfolio distribution by sectors:
2075        view["analytics"]["distrBySectors"]["All money cash"] = {
2076            "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"],
2077            "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"],
2078        }
2079        view["analytics"]["distrBySectors"].update(bySect)
2080
2081        # portfolio distribution by currencies:
2082        if "rub" not in view["analytics"]["distrByCurrencies"].keys():
2083            view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0}
2084
2085            if self.moreDebug:
2086                uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!")
2087
2088        view["analytics"]["distrByCurrencies"].update(byCurr)
2089        view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2090        view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2091
2092        # portfolio distribution by countries:
2093        if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys():
2094            view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0}
2095
2096            if self.moreDebug:
2097                uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!")
2098
2099        view["analytics"]["distrByCountries"].update(byCountry)
2100        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2101        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2102
2103        # --- Prepare text statistics overview in human-readable:
2104        if show:
2105            actualOnDate = datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)
2106
2107            # Whatever the value `details`, header not changes:
2108            info = [
2109                "# Client's portfolio\n\n",
2110                "* **Actual on date:** [{} UTC]\n".format(actualOnDate),
2111                "* **Account ID:** [{}]\n".format(self.accountId),
2112            ]
2113
2114            if details in ["full", "positions", "digest"]:
2115                info.extend([
2116                    "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2117                    "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format(
2118                        "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2119                        view["stat"]["totalChangesRUB"],
2120                        "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2121                        view["stat"]["totalChangesPercentRUB"],
2122                    ),
2123                ])
2124
2125            if details in ["full", "positions"]:
2126                info.extend([
2127                    "## Open positions\n\n",
2128                    "| Ticker [FIGI]               | Volume (blocked)                | Lots     | Curr. price  | Avg. price   | Current volume cost | Profit (%)                   |\n",
2129                    "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n",
2130                    "| **Ruble:**                  | {:>31} |          |              |              |                     |                              |\n".format(
2131                        "{:.2f} ({:.2f}) rub".format(
2132                            view["stat"]["availableRUB"],
2133                            view["stat"]["blockedRUB"],
2134                        )
2135                    )
2136                ])
2137
2138                def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list:
2139                    return [
2140                        "|                             |                                 |          |              |              |                     |                              |\n",
2141                        "| {:<27} |                                 |          |              |              | {:>19} |                              |\n".format(
2142                            noTradeStr if noTradeStr else typeStr,
2143                            "" if noTradeStr else "{:.2f} RUB".format(CostRUB),
2144                        ),
2145                    ]
2146
2147                def _InfoStr(data: dict, isCurr: bool = False) -> str:
2148                    return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format(
2149                        "{} [{}]".format(data["ticker"], data["figi"]),
2150                        "{:.2f} ({:.2f}) {}".format(
2151                            data["volume"],
2152                            data["blocked"],
2153                            data["currency"],
2154                        ) if isCurr else "{:.0f} ({:.0f})".format(
2155                            data["volume"],
2156                            data["blocked"],
2157                        ),
2158                        "—" if isCurr else "{:.4f}".format(data["lots"]).rstrip("0").rstrip("."),
2159                        "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a",
2160                        "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a",
2161                        "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]),
2162                        "{}{:.2f} {} ({}{:.2f}%)".format(
2163                            "+" if data["profit"] > 0 else "",
2164                            data["profit"], data["baseCurrencyName"],
2165                            "+" if data["percentProfit"] > 0 else "",
2166                            data["percentProfit"],
2167                        ),
2168                    )
2169
2170                # --- Show currencies section:
2171                if view["stat"]["Currencies"]:
2172                    info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**"))
2173                    for item in view["stat"]["Currencies"]:
2174                        info.append(_InfoStr(item, isCurr=True))
2175
2176                else:
2177                    info.extend(_SplitStr(noTradeStr="**Currencies:** no trades"))
2178
2179                # --- Show shares section:
2180                if view["stat"]["Shares"]:
2181                    info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**"))
2182
2183                    for item in view["stat"]["Shares"]:
2184                        info.append(_InfoStr(item))
2185
2186                else:
2187                    info.extend(_SplitStr(noTradeStr="**Shares:** no trades"))
2188
2189                # --- Show bonds section:
2190                if view["stat"]["Bonds"]:
2191                    info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**"))
2192
2193                    for item in view["stat"]["Bonds"]:
2194                        info.append(_InfoStr(item))
2195
2196                else:
2197                    info.extend(_SplitStr(noTradeStr="**Bonds:** no trades"))
2198
2199                # --- Show etfs section:
2200                if view["stat"]["Etfs"]:
2201                    info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**"))
2202
2203                    for item in view["stat"]["Etfs"]:
2204                        info.append(_InfoStr(item))
2205
2206                else:
2207                    info.extend(_SplitStr(noTradeStr="**Etfs:** no trades"))
2208
2209                # --- Show futures section:
2210                if view["stat"]["Futures"]:
2211                    info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**"))
2212
2213                    for item in view["stat"]["Futures"]:
2214                        info.append(_InfoStr(item))
2215
2216                else:
2217                    info.extend(_SplitStr(noTradeStr="**Futures:** no trades"))
2218
2219            if details in ["full", "orders"]:
2220                # --- Show pending limit orders section:
2221                if view["stat"]["orders"]:
2222                    info.extend([
2223                        "\n## Opened pending limit-orders: [{}]\n".format(len(view["stat"]["orders"])),
2224                        "\n| Ticker [FIGI]               | Order ID       | Lots (exec.) | Current price (% delta) | Target price  | Action    | Type      | Create date (UTC)       |\n",
2225                        "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n",
2226                    ])
2227
2228                    for item in view["stat"]["orders"]:
2229                        info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format(
2230                            "{} [{}]".format(item["ticker"], item["figi"]),
2231                            item["orderID"],
2232                            "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]),
2233                            "{} {} ({}{:.2f}%)".format(
2234                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2235                                item["baseCurrencyName"],
2236                                "+" if item["percentChanges"] > 0 else "",
2237                                float(item["percentChanges"]),
2238                            ),
2239                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2240                            item["action"],
2241                            item["type"],
2242                            item["date"],
2243                        ))
2244
2245                else:
2246                    info.append("\n## Total pending limit-orders: [0]\n")
2247
2248                # --- Show stop orders section:
2249                if view["stat"]["stopOrders"]:
2250                    info.extend([
2251                        "\n## Opened stop-orders: [{}]\n".format(len(view["stat"]["stopOrders"])),
2252                        "\n| Ticker [FIGI]               | Stop order ID                        | Lots   | Current price (% delta) | Target price  | Limit price   | Action    | Type        | Expire type  | Create date (UTC)   | Expiration (UTC)    |\n",
2253                        "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n",
2254                    ])
2255
2256                    for item in view["stat"]["stopOrders"]:
2257                        info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format(
2258                            "{} [{}]".format(item["ticker"], item["figi"]),
2259                            item["orderID"],
2260                            item["lotsRequested"],
2261                            "{} {} ({}{:.2f}%)".format(
2262                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2263                                item["baseCurrencyName"],
2264                                "+" if item["percentChanges"] > 0 else "",
2265                                float(item["percentChanges"]),
2266                            ),
2267                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2268                            "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"],
2269                            item["action"],
2270                            item["type"],
2271                            item["expType"],
2272                            item["createDate"],
2273                            item["expDate"],
2274                        ))
2275
2276                else:
2277                    info.append("\n## Total stop-orders: [0]\n")
2278
2279            if details in ["full", "analytics"]:
2280                # -- Show analytics section:
2281                if view["stat"]["portfolioCostRUB"] > 0:
2282                    info.extend([
2283                        "\n# Analytics\n\n"
2284                        "* **Actual on date:** [{} UTC]\n".format(actualOnDate),
2285                        "* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2286                        "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format(
2287                            "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2288                            view["stat"]["totalChangesRUB"],
2289                            "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2290                            view["stat"]["totalChangesPercentRUB"],
2291                        ),
2292                        "\n## Portfolio distribution by assets\n"
2293                        "\n| Type                               | Uniques | Percent | Current cost       |\n",
2294                        "|------------------------------------|---------|---------|--------------------|\n",
2295                    ])
2296
2297                    for key in view["analytics"]["distrByAssets"].keys():
2298                        if view["analytics"]["distrByAssets"][key]["cost"] > 0:
2299                            info.append("| {:<34} | {:<7} | {:<7} | {:<18} |\n".format(
2300                                key,
2301                                view["analytics"]["distrByAssets"][key]["uniques"],
2302                                "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]),
2303                                "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]),
2304                            ))
2305
2306                    aSepLine = "|----------------------------------------------|---------|--------------------|\n"
2307
2308                    info.extend([
2309                        "\n## Portfolio distribution by companies\n"
2310                        "\n| Company                                      | Percent | Current cost       |\n",
2311                        aSepLine,
2312                    ])
2313
2314                    for company in view["analytics"]["distrByCompanies"].keys():
2315                        if view["analytics"]["distrByCompanies"][company]["cost"] > 0:
2316                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2317                                "{}{}".format(
2318                                    "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "",
2319                                    company,
2320                                ),
2321                                "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]),
2322                                "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]),
2323                            ))
2324
2325                    info.extend([
2326                        "\n## Portfolio distribution by sectors\n"
2327                        "\n| Sector                                       | Percent | Current cost       |\n",
2328                        aSepLine,
2329                    ])
2330
2331                    for sector in view["analytics"]["distrBySectors"].keys():
2332                        if view["analytics"]["distrBySectors"][sector]["cost"] > 0:
2333                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2334                                sector,
2335                                "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]),
2336                                "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]),
2337                            ))
2338
2339                    info.extend([
2340                        "\n## Portfolio distribution by currencies\n"
2341                        "\n| Instruments currencies                       | Percent | Current cost       |\n",
2342                        aSepLine,
2343                    ])
2344
2345                    for curr in view["analytics"]["distrByCurrencies"].keys():
2346                        if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0:
2347                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2348                                "[{}] {}".format(curr, view["analytics"]["distrByCurrencies"][curr]["name"]),
2349                                "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]),
2350                                "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]),
2351                            ))
2352
2353                    info.extend([
2354                        "\n## Portfolio distribution by countries\n"
2355                        "\n| Assets by country                            | Percent | Current cost       |\n",
2356                        aSepLine,
2357                    ])
2358
2359                    for country in view["analytics"]["distrByCountries"].keys():
2360                        if view["analytics"]["distrByCountries"][country]["cost"] > 0:
2361                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2362                                country,
2363                                "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]),
2364                                "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]),
2365                            ))
2366
2367            if details in ["full", "calendar"]:
2368                # -- Show bonds payment calendar section:
2369                if view["stat"]["Bonds"]:
2370                    bondTickers = [item["ticker"] for item in view["stat"]["Bonds"]]
2371                    view["analytics"]["bondsCalendar"] = self.ExtendBondsData(instruments=bondTickers, xlsx=False)
2372                    info.append("\n" + self.ShowBondsCalendar(extBonds=view["analytics"]["bondsCalendar"], show=False))
2373
2374                else:
2375                    info.append("\n# Bond payments calendar\n\nNo bonds in the portfolio to create payments calendar\n")
2376
2377            infoText = "".join(info)
2378
2379            uLogger.info(infoText)
2380
2381            if details == "full" and self.overviewFile:
2382                filename = self.overviewFile
2383
2384            elif details == "digest" and self.overviewDigestFile:
2385                filename = self.overviewDigestFile
2386
2387            elif details == "positions" and self.overviewPositionsFile:
2388                filename = self.overviewPositionsFile
2389
2390            elif details == "orders" and self.overviewOrdersFile:
2391                filename = self.overviewOrdersFile
2392
2393            elif details == "analytics" and self.overviewAnalyticsFile:
2394                filename = self.overviewAnalyticsFile
2395
2396            elif details == "calendar" and self.overviewBondsCalendarFile:
2397                filename = self.overviewBondsCalendarFile
2398
2399            else:
2400                filename = ""
2401
2402            if filename:
2403                with open(filename, "w", encoding="UTF-8") as fH:
2404                    fH.write(infoText)
2405
2406                uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename)))
2407
2408                if self.useHTMLReports:
2409                    htmlFilePath = filename.replace(".md", ".html") if filename.endswith(".md") else filename + ".html"
2410                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
2411                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Client's portfolio", commonCSS=COMMON_CSS, markdown=infoText))
2412
2413                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
2414
2415        return view
2416
2417    def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple[list[dict], dict]:
2418        """
2419        Returns history operations between two given dates for current `accountId`.
2420        If `reportFile` string is not empty then also save human-readable report.
2421        Shows some statistical data of closed positions.
2422
2423        :param start: see docstring in `TradeRoutines.GetDatesAsString()` method.
2424        :param end: see docstring in `TradeRoutines.GetDatesAsString()` method.
2425        :param show: if `True` then also prints all records to the console.
2426        :param showCancelled: if `False` then remove information about cancelled operations from the deals report.
2427        :return: original list of dictionaries with history of deals records from API ("operations" key):
2428                 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2429                 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.
2430        """
2431        if self.accountId is None or not self.accountId:
2432            uLogger.error("Variable `accountId` must be defined for using this method!")
2433            raise Exception("Account ID required")
2434
2435        startDate, endDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT)  # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2436
2437        uLogger.debug("Requesting history of a client's operations. Wait, please...")
2438
2439        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2440        dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations"
2441        self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate})
2442        ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"]  # list of dict: operations returns by broker
2443        customStat = {}  # custom statistics in additional to responseJSON
2444
2445        # --- output report in human-readable format:
2446        if show or self.reportFile:
2447            splitLine1 = "|                            |                               |                              |                      |                        |\n"  # Summary section
2448            splitLine2 = "|                     |              |              |            |           |                 |            |                                                                    |\n"  # Operations section
2449            nextDay = ""
2450
2451            info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])]
2452
2453            if len(ops) > 0:
2454                customStat = {
2455                    "opsCount": 0,  # total operations count
2456                    "buyCount": 0,  # buy operations
2457                    "sellCount": 0,  # sell operations
2458                    "buyTotal": {"rub": 0.},  # Buy sums in different currencies
2459                    "sellTotal": {"rub": 0.},  # Sell sums in different currencies
2460                    "payIn": {"rub": 0.},  # Deposit brokerage account
2461                    "payOut": {"rub": 0.},  # Withdrawals
2462                    "divs": {"rub": 0.},  # Dividends income
2463                    "coupons": {"rub": 0.},  # Coupon's income
2464                    "brokerCom": {"rub": 0.},  # Service commissions
2465                    "serviceCom": {"rub": 0.},  # Service commissions
2466                    "marginCom": {"rub": 0.},  # Margin commissions
2467                    "allTaxes": {"rub": 0.},  # Sum of withholding taxes and corrections
2468                }
2469
2470                # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES:
2471                for item in ops:
2472                    if item["state"] == "OPERATION_STATE_EXECUTED":
2473                        payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2474
2475                        # count buy operations:
2476                        if "_BUY" in item["operationType"]:
2477                            customStat["buyCount"] += 1
2478
2479                            if item["payment"]["currency"] in customStat["buyTotal"].keys():
2480                                customStat["buyTotal"][item["payment"]["currency"]] += payment
2481
2482                            else:
2483                                customStat["buyTotal"][item["payment"]["currency"]] = payment
2484
2485                        # count sell operations:
2486                        elif "_SELL" in item["operationType"]:
2487                            customStat["sellCount"] += 1
2488
2489                            if item["payment"]["currency"] in customStat["sellTotal"].keys():
2490                                customStat["sellTotal"][item["payment"]["currency"]] += payment
2491
2492                            else:
2493                                customStat["sellTotal"][item["payment"]["currency"]] = payment
2494
2495                        # count incoming operations:
2496                        elif item["operationType"] in ["OPERATION_TYPE_INPUT"]:
2497                            if item["payment"]["currency"] in customStat["payIn"].keys():
2498                                customStat["payIn"][item["payment"]["currency"]] += payment
2499
2500                            else:
2501                                customStat["payIn"][item["payment"]["currency"]] = payment
2502
2503                        # count withdrawals operations:
2504                        elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]:
2505                            if item["payment"]["currency"] in customStat["payOut"].keys():
2506                                customStat["payOut"][item["payment"]["currency"]] += payment
2507
2508                            else:
2509                                customStat["payOut"][item["payment"]["currency"]] = payment
2510
2511                        # count dividends income:
2512                        elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]:
2513                            if item["payment"]["currency"] in customStat["divs"].keys():
2514                                customStat["divs"][item["payment"]["currency"]] += payment
2515
2516                            else:
2517                                customStat["divs"][item["payment"]["currency"]] = payment
2518
2519                        # count coupon's income:
2520                        elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]:
2521                            if item["payment"]["currency"] in customStat["coupons"].keys():
2522                                customStat["coupons"][item["payment"]["currency"]] += payment
2523
2524                            else:
2525                                customStat["coupons"][item["payment"]["currency"]] = payment
2526
2527                        # count broker commissions:
2528                        elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]:
2529                            if item["payment"]["currency"] in customStat["brokerCom"].keys():
2530                                customStat["brokerCom"][item["payment"]["currency"]] += payment
2531
2532                            else:
2533                                customStat["brokerCom"][item["payment"]["currency"]] = payment
2534
2535                        # count service commissions:
2536                        elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]:
2537                            if item["payment"]["currency"] in customStat["serviceCom"].keys():
2538                                customStat["serviceCom"][item["payment"]["currency"]] += payment
2539
2540                            else:
2541                                customStat["serviceCom"][item["payment"]["currency"]] = payment
2542
2543                        # count margin commissions:
2544                        elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]:
2545                            if item["payment"]["currency"] in customStat["marginCom"].keys():
2546                                customStat["marginCom"][item["payment"]["currency"]] += payment
2547
2548                            else:
2549                                customStat["marginCom"][item["payment"]["currency"]] = payment
2550
2551                        # count withholding taxes:
2552                        elif "_TAX" in item["operationType"]:
2553                            if item["payment"]["currency"] in customStat["allTaxes"].keys():
2554                                customStat["allTaxes"][item["payment"]["currency"]] += payment
2555
2556                            else:
2557                                customStat["allTaxes"][item["payment"]["currency"]] = payment
2558
2559                        else:
2560                            continue
2561
2562                customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"]
2563
2564                # --- view "Actions" lines:
2565                info.extend([
2566                    "| Report sections            |                               |                              |                      |                        |\n",
2567                    "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n",
2568                    "| **Actions:**               | Trades: {:<21} | Trading volumes:             |                      |                        |\n".format(customStat["opsCount"]),
2569                    "|                            |   Buy: {:<22} | {:<28} |                      |                        |\n".format(
2570                        "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2571                        "  rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else "  —",
2572                    ),
2573                    "|                            |   Sell: {:<21} | {:<28} |                      |                        |\n".format(
2574                        "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2575                        "  rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else "  —",
2576                    ),
2577                ])
2578
2579                opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys()))))
2580                for key in opsKeys:
2581                    if key == "rub":
2582                        continue
2583
2584                    info.extend([
2585                        "|                            |                               | {:<28} |                      |                        |\n".format(
2586                            "  {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0)
2587                        ),
2588                        "|                            |                               | {:<28} |                      |                        |\n".format(
2589                            "  {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0)
2590                        ),
2591                    ])
2592
2593                info.append(splitLine1)
2594
2595                def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str:
2596                    return "|                            | {:<29} | {:<28} | {:<20} | {:<22} |\n".format(
2597                            "  {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else "  —",
2598                            "  {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else "  —",
2599                            "  {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else "  —",
2600                            "  {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else "  —",
2601                    )
2602
2603                # --- view "Payments" lines:
2604                info.append("| **Payments:**              | Deposit on broker account:    | Withdrawals:                 | Dividends income:    | Coupons income:        |\n")
2605                paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys()))))
2606
2607                for key in paymentsKeys:
2608                    info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key))
2609
2610                info.append(splitLine1)
2611
2612                # --- view "Commissions and taxes" lines:
2613                info.append("| **Commissions and taxes:** | Broker commissions:           | Service commissions:         | Margin commissions:  | All taxes/corrections: |\n")
2614                comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys()))))
2615
2616                for key in comKeys:
2617                    info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key))
2618
2619                info.extend([
2620                    "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"),
2621                    "| Date and time       | FIGI         | Ticker       | Asset      | Value     | Payment         | Status     | Operation type                                                     |\n",
2622                    "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n",
2623                ])
2624
2625            else:
2626                info.append("Broker returned no operations during this period\n")
2627
2628            # --- view "Operations" section:
2629            for item in ops:
2630                if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]:
2631                    continue
2632
2633                else:
2634                    self._figi = item["figi"]
2635                    payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2636                    instrument = self.SearchByFIGI(requestPrice=False) if self._figi else {}
2637
2638                    # group of deals during one day:
2639                    if nextDay and item["date"].split("T")[0] != nextDay:
2640                        info.append(splitLine2)
2641                        nextDay = ""
2642
2643                    else:
2644                        nextDay = item["date"].split("T")[0]  # saving current day for splitting
2645
2646                    info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format(
2647                        item["date"].replace("T", " ").replace("Z", "").split(".")[0],
2648                        self._figi if self._figi else "—",
2649                        instrument["ticker"] if instrument else "—",
2650                        instrument["type"] if instrument else "—",
2651                        item["quantity"] if int(item["quantity"]) > 0 else "—",
2652                        "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—",
2653                        TKS_OPERATION_STATES[item["state"]],
2654                        TKS_OPERATION_TYPES[item["operationType"]],
2655                    ))
2656
2657            infoText = "".join(info)
2658
2659            if show:
2660                if self.moreDebug:
2661                    uLogger.debug("Records about history of a client's operations successfully received")
2662
2663                uLogger.info(infoText)
2664
2665            if self.reportFile:
2666                with open(self.reportFile, "w", encoding="UTF-8") as fH:
2667                    fH.write(infoText)
2668
2669                uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile)))
2670
2671                if self.useHTMLReports:
2672                    htmlFilePath = self.reportFile.replace(".md", ".html") if self.reportFile.endswith(".md") else self.reportFile + ".html"
2673                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
2674                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Client's operations", commonCSS=COMMON_CSS, markdown=infoText))
2675
2676                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
2677
2678        return ops, customStat
2679
2680    def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False) -> pd.DataFrame:
2681        """
2682        This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id).
2683
2684        History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`.
2685        Warning! Broker server used ISO UTC time by default.
2686
2687        If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame.
2688        Also, `historyFile` used to update history with `onlyMissing` parameter.
2689
2690        See also: `LoadHistory()` and `ShowHistoryChart()` methods.
2691
2692        :param start: see docstring in `TradeRoutines.GetDatesAsString()` method.
2693        :param end: see docstring in `TradeRoutines.GetDatesAsString()` method.
2694        :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`,
2695                         `"hour"`, `"day"`. Default: `"hour"`.
2696        :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`.
2697                            False by default. Warning! History appends only from last candle to current time
2698                            with always update last candle!
2699        :param csvSep: separator if csv-file is used, `,` by default.
2700        :param show: if `True` then also prints Pandas DataFrame to the console.
2701        :return: Pandas DataFrame with prices history. Headers of columns are defined by default:
2702                 `["date", "time", "open", "high", "low", "close", "volume"]`.
2703        """
2704        strStartDate, strEndDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT)  # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2705        headers = ["date", "time", "open", "high", "low", "close", "volume"]  # sequence and names of column headers
2706        history = None  # empty pandas object for history
2707
2708        if interval not in TKS_CANDLE_INTERVALS.keys():
2709            uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.")
2710            raise Exception("Incorrect value")
2711
2712        if not (self._ticker or self._figi):
2713            uLogger.error("Ticker or FIGI must be defined!")
2714            raise Exception("Ticker or FIGI required")
2715
2716        if self._ticker and not self._figi:
2717            instrumentByTicker = self.SearchByTicker(requestPrice=False)
2718            self._figi = instrumentByTicker["figi"] if instrumentByTicker else ""
2719
2720        if self._figi and not self._ticker:
2721            instrumentByFIGI = self.SearchByFIGI(requestPrice=False)
2722            self._ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else ""
2723
2724        dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from start time string
2725        dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from end time string
2726        if interval.lower() != "day":
2727            dtEnd += timedelta(seconds=1)  # adds 1 sec for requests, because day end returned by `TradeRoutines.GetDatesAsString()` is 23:59:59
2728
2729        delta = dtEnd - dtStart  # current UTC time minus last time in file
2730        deltaMinutes = delta.days * 1440 + delta.seconds // 60  # minutes between start and end dates
2731
2732        # calculate history length in candles:
2733        length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1]
2734        if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0:
2735            length += 1  # to avoid fraction time
2736
2737        # calculate data blocks count:
2738        blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2]
2739
2740        uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end))
2741        uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate))
2742        uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval))
2743        uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2]))
2744        uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self._ticker, self._figi))
2745
2746        tempOld = None  # pandas object for old history, if --only-missing key present
2747        lastTime = None  # datetime object of last old candle in file
2748
2749        if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile):
2750            uLogger.debug("--only-missing key present, add only last missing candles...")
2751            uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile)))
2752
2753            tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers)
2754
2755            tempOld["date"] = pd.to_datetime(tempOld["date"])  # load date "as is"
2756            tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d")  # convert date to string
2757            tempOld["time"] = pd.to_datetime(tempOld["time"])  # load time "as is"
2758            tempOld["time"] = tempOld["time"].dt.strftime("%H:%M")  # convert time to string
2759
2760            # get last datetime object from last string in file or minus 1 delta if file is empty:
2761            if len(tempOld) > 0:
2762                lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2763
2764            else:
2765                lastTime = dtEnd - timedelta(days=1)  # history file is empty, so last date set at -1 day
2766
2767            tempOld = tempOld[:-1]  # always remove last old candle because it may be incompletely at the current time
2768
2769        responseJSONs = []  # raw history blocks of data
2770
2771        blockEnd = dtEnd
2772        for item in range(blocks):
2773            tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2]
2774            blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail)
2775
2776            uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format(
2777                item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2778            ))
2779
2780            if blockStart == blockEnd:
2781                uLogger.debug("Skipped this zero-length block...")
2782
2783            else:
2784                # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles
2785                historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles"
2786                self.body = str({
2787                    "figi": self._figi,
2788                    "from": blockStart.strftime(TKS_DATE_TIME_FORMAT),
2789                    "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2790                    "interval": TKS_CANDLE_INTERVALS[interval][0]
2791                })
2792                responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1)
2793
2794                if "code" in responseJSON.keys():
2795                    uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks))
2796
2797                else:
2798                    if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1:
2799                        responseJSON["candles"] = responseJSON["candles"][:-1]  # removes last candle for "yesterday" request
2800
2801                    responseJSONs = responseJSON["candles"] + responseJSONs  # add more old history behind newest dates
2802
2803            blockEnd = blockStart
2804
2805        printCount = len(responseJSONs)  # candles to show in console
2806        if responseJSONs:
2807            tempHistory = pd.DataFrame(
2808                data={
2809                    "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2810                    "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2811                    "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs],
2812                    "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs],
2813                    "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs],
2814                    "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs],
2815                    "volume": [int(item["volume"]) for item in responseJSONs],
2816                },
2817                index=range(len(responseJSONs)),
2818                columns=["date", "time", "open", "high", "low", "close", "volume"],
2819            )
2820            tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d")
2821            tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M")
2822
2823            # append only newest candles to old history if --only-missing key present:
2824            if onlyMissing and tempOld is not None and lastTime is not None:
2825                index = 0  # find start index in tempHistory data:
2826
2827                for i, item in tempHistory.iterrows():
2828                    curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2829
2830                    if curTime == lastTime:
2831                        uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
2832                        index = i
2833                        printCount = index + 1
2834                        break
2835
2836                history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True)
2837
2838            else:
2839                history = tempHistory  # if no `--only-missing` key then load full data from server
2840
2841            uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False)))
2842
2843        if history is not None and not history.empty:
2844            if show:
2845                uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format(
2846                    strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]),
2847                    pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False),
2848                ))
2849
2850        else:
2851            uLogger.warning("Received an empty candles history!")
2852
2853        if self.historyFile is not None:
2854            if history is not None and not history.empty:
2855                history.to_csv(self.historyFile, sep=csvSep, index=False, header=None)
2856                uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self._ticker, self._figi, interval, os.path.abspath(self.historyFile)))
2857
2858            else:
2859                uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile)))
2860
2861        else:
2862            uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.")
2863
2864        return history
2865
2866    def LoadHistory(self, filePath: str) -> pd.DataFrame:
2867        """
2868        Load candles history from csv-file and return Pandas DataFrame object.
2869
2870        See also: `History()` and `ShowHistoryChart()` methods.
2871
2872        :param filePath: path to csv-file to open.
2873        """
2874        loadedHistory = None  # init candles data object
2875
2876        uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...")
2877
2878        if os.path.exists(filePath):
2879            loadedHistory = self.priceModel.LoadFromFile(filePath)  # load data and get chain of candles as Pandas DataFrame
2880
2881            tfStr = self.priceModel.FormattedDelta(
2882                self.priceModel.timeframe,
2883                "{days} days {hours}h {minutes}m {seconds}s",
2884            ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta(
2885                self.priceModel.timeframe,
2886                "{hours}h {minutes}m {seconds}s",
2887            )
2888
2889            if loadedHistory is not None and not loadedHistory.empty:
2890                uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format(
2891                    len(loadedHistory),
2892                    tfStr,
2893                    pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)),
2894                )
2895
2896            else:
2897                uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath)))
2898
2899        else:
2900            uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath))
2901
2902        return loadedHistory
2903
2904    def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None:
2905        """
2906        Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.
2907
2908        Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart.
2909        Default: `index.html` (both for interact and non-interact candlesticks chart).
2910
2911        See also: `History()` and `LoadHistory()` methods.
2912
2913        :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object.
2914        :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart.
2915                         See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters
2916                         If False then chain of candlesticks will render as not interactive Google Candlestick chart.
2917                         See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template
2918        :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to
2919                              html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file.
2920        """
2921        if isinstance(candles, str):
2922            self.priceModel.prices = self.LoadHistory(filePath=candles)  # load candles chain from file
2923            self.priceModel.ticker = os.path.basename(candles)  # use filename as ticker name in PriceGenerator
2924
2925        elif isinstance(candles, pd.DataFrame):
2926            self.priceModel.prices = candles  # set candles chain from variable
2927            self.priceModel.ticker = self._ticker  # use current TKSBrokerAPI ticker as ticker name in PriceGenerator
2928
2929            if "datetime" not in candles.columns:
2930                self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True)  # PriceGenerator uses "datetime" column with date and time
2931
2932        else:
2933            uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!")
2934            raise Exception("Incorrect value")
2935
2936        self.priceModel.horizon = len(self.priceModel.prices)  # use length of candles data as horizon in PriceGenerator
2937
2938        if interact:
2939            uLogger.debug("Rendering interactive candles chart. Wait, please...")
2940
2941            self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2942
2943        else:
2944            uLogger.debug("Rendering non-interactive candles chart. Wait, please...")
2945
2946            self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2947
2948        uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile)))
2949
2950    def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2951        """
2952        Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response.
2953        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
2954
2955        See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`.
2956
2957        :param operation: string "Buy" or "Sell".
2958        :param lots: volume, integer count of lots >= 1.
2959        :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`.
2960        :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`.
2961        :param expDate: string "Undefined" by default or local date in future,
2962                        it is a string with format `%Y-%m-%d %H:%M:%S`.
2963        :return: JSON with response from broker server.
2964        """
2965        if self.accountId is None or not self.accountId:
2966            uLogger.error("Variable `accountId` must be defined for using this method!")
2967            raise Exception("Account ID required")
2968
2969        if operation is None or not operation or operation not in ("Buy", "Sell"):
2970            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
2971            raise Exception("Incorrect value")
2972
2973        if lots is None or lots < 1:
2974            uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.")
2975            lots = 1
2976
2977        if tp is None or tp < 0:
2978            tp = 0
2979
2980        if sl is None or sl < 0:
2981            sl = 0
2982
2983        if expDate is None or not expDate:
2984            expDate = "Undefined"
2985
2986        if not (self._ticker or self._figi):
2987            uLogger.error("Ticker or FIGI must be defined!")
2988            raise Exception("Ticker or FIGI required")
2989
2990        instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True)
2991        self._ticker = instrument["ticker"]
2992        self._figi = instrument["figi"]
2993
2994        uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self._ticker, self._figi, lots, tp, sl, expDate))
2995
2996        openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
2997        self.body = str({
2998            "figi": self._figi,
2999            "quantity": str(lots),
3000            "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
3001            "accountId": str(self.accountId),
3002            "orderType": "ORDER_TYPE_MARKET",  # see: TKS_ORDER_TYPES
3003        })
3004        response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0)
3005
3006        if "orderId" in response.keys():
3007            uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format(
3008                operation, response["orderId"],
3009                self._ticker, self._figi, lots,
3010                NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"],
3011                NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"],
3012                NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"],
3013            ))
3014
3015            if tp > 0:
3016                self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate)
3017
3018            if sl > 0:
3019                self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate)
3020
3021        else:
3022            uLogger.warning("Not `oK` status received! Market order not executed. See full debug log and try again open order later.")
3023
3024        return response
3025
3026    def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
3027        """
3028        More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response.
3029        If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter.
3030
3031        See also: `Order()` and `Trade()` docstrings.
3032
3033        :param lots: volume, integer count of lots >= 1.
3034        :param tp: float > 0, take profit price of stop-order.
3035        :param sl: float > 0, stop loss price of stop-order.
3036        :param expDate: it's a local date in future.
3037                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3038        :return: JSON with response from broker server.
3039        """
3040        return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate)
3041
3042    def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
3043        """
3044        More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response.
3045        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
3046
3047        See also: `Order()` and `Trade()` docstrings.
3048
3049        :param lots: volume, integer count of lots >= 1.
3050        :param tp: float > 0, take profit price of stop-order.
3051        :param sl: float > 0, stop loss price of stop-order.
3052        :param expDate: it's a local date in the future.
3053                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3054        :return: JSON with response from broker server.
3055        """
3056        return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate)
3057
3058    def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None:
3059        """
3060        Close position of given instruments.
3061
3062        :param instruments: list of instruments defined by tickers or FIGIs that must be closed.
3063        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
3064                         This avoids unnecessary downloading data from the server.
3065        """
3066        if instruments is None or not instruments:
3067            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
3068            raise Exception("Ticker or FIGI required")
3069
3070        if isinstance(instruments, str):
3071            instruments = [instruments]
3072
3073        uniqueInstruments = self.GetUniqueFIGIs(instruments)
3074        if uniqueInstruments:
3075            if portfolio is None or not portfolio:
3076                portfolio = self.Overview(show=False)
3077
3078            allOpened = [item["figi"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]]
3079            uLogger.debug("All opened instruments by it's FIGI: {}".format(", ".join(allOpened)))
3080
3081            for self._figi in uniqueInstruments:
3082                if self._figi not in allOpened:
3083                    uLogger.warning("Instrument with FIGI [{}] not in open positions list!".format(self._figi))
3084                    continue
3085
3086                # search open trade info about instrument by ticker:
3087                instrument = {}
3088                for iType in TKS_INSTRUMENTS:
3089                    if instrument:
3090                        break
3091
3092                    for item in portfolio["stat"][iType]:
3093                        if item["figi"] == self._figi:
3094                            instrument = item
3095                            break
3096
3097                if instrument:
3098                    self._ticker = instrument["ticker"]
3099                    self._figi = instrument["figi"]
3100
3101                    uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format(
3102                        self._ticker,
3103                        self._figi,
3104                        int(instrument["volume"]),
3105                        ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "",
3106                    ))
3107
3108                    tradeLots = abs(instrument["lots"]) - instrument["blocked"]  # available volumes in lots for close operation
3109
3110                    if tradeLots > 0:
3111                        if instrument["blocked"] > 0:
3112                            uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format(
3113                                instrument["blocked"],
3114                                self._ticker,
3115                                tradeLots,
3116                            ))
3117
3118                        # if direction is "Long" then we need sell, if direction is "Short" then we need buy:
3119                        self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots)
3120
3121                    else:
3122                        uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self._ticker))
3123
3124    def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None:
3125        """
3126        Close all positions of given instruments with defined type.
3127
3128        :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
3129        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
3130                         This avoids unnecessary downloading data from the server.
3131        """
3132        if iType not in TKS_INSTRUMENTS:
3133            uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType))
3134
3135        else:
3136            if portfolio is None or not portfolio:
3137                portfolio = self.Overview(show=False)
3138
3139            tickers = [item["ticker"] for item in portfolio["stat"][iType]]
3140            uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers))
3141
3142            if tickers and portfolio:
3143                self.CloseTrades(tickers, portfolio)
3144
3145            else:
3146                uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType))
3147
3148    def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3149        """
3150        Universal method to create market or limit orders with all available parameters for current `accountId`.
3151        See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`.
3152
3153        If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above
3154        current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.
3155
3156        Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell"
3157        then broker immediately open market order as you can do simple --buy or --sell operations!
3158
3159        If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell".
3160        When current price will go up or down to target price value then broker opens a limit order.
3161        Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.
3162
3163        Only one attempt and no retry for opens order. If network issue occurred you can create new request.
3164
3165        :param operation: string "Buy" or "Sell".
3166        :param orderType: string "Limit" or "Stop".
3167        :param lots: volume, integer count of lots >= 1.
3168        :param targetPrice: target price > 0. This is open trade price for limit order.
3169        :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice.
3170                           Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
3171        :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types
3172                         "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3173                         Stop loss order always executed by market price.
3174        :param expDate: string "Undefined" by default or local date in future.
3175                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3176                        This date is converting to UTC format for server. This parameter only makes sense for stop-order.
3177                        A limit order has no expiration date, it lasts until the end of the trading day.
3178        :return: JSON with response from broker server.
3179        """
3180        if self.accountId is None or not self.accountId:
3181            uLogger.error("Variable `accountId` must be defined for using this method!")
3182            raise Exception("Account ID required")
3183
3184        if operation is None or not operation or operation not in ("Buy", "Sell"):
3185            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
3186            raise Exception("Incorrect value")
3187
3188        if orderType is None or not orderType or orderType not in ("Limit", "Stop"):
3189            uLogger.error("You must define order type only one of them: `Limit` or `Stop`!")
3190            raise Exception("Incorrect value")
3191
3192        if lots is None or lots < 1:
3193            uLogger.error("You must define trade volume > 0: integer count of lots!")
3194            raise Exception("Incorrect value")
3195
3196        if targetPrice is None or targetPrice <= 0:
3197            uLogger.error("Target price for limit-order must be greater than 0!")
3198            raise Exception("Incorrect value")
3199
3200        if limitPrice is None or limitPrice <= 0:
3201            limitPrice = targetPrice
3202
3203        if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"):
3204            stopType = "Limit"
3205
3206        if expDate is None or not expDate:
3207            expDate = "Undefined"
3208
3209        if not (self._ticker or self._figi):
3210            uLogger.error("Tocker or FIGI must be defined!")
3211            raise Exception("Ticker or FIGI required")
3212
3213        response = {}
3214        instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True)
3215        self._ticker = instrument["ticker"]
3216        self._figi = instrument["figi"]
3217
3218        if orderType == "Limit":
3219            uLogger.debug(
3220                "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format(
3221                    self._ticker, self._figi,
3222                    operation, lots, targetPrice, instrument["currency"],
3223                ))
3224
3225            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
3226            self.body = str({
3227                "figi": self._figi,
3228                "quantity": str(lots),
3229                "price": FloatToNano(targetPrice),
3230                "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
3231                "accountId": str(self.accountId),
3232                "orderType": "ORDER_TYPE_LIMIT",  # see: TKS_ORDER_TYPES
3233            })
3234            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0)
3235
3236            if "orderId" in response.keys():
3237                uLogger.info(
3238                    "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{} {}]".format(
3239                        response["orderId"], self._ticker, self._figi, operation, lots,
3240                        "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"],
3241                    ))
3242
3243                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3244                    if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]:
3245                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format(
3246                            targetPrice, instrument["currency"],
3247                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3248                        ))
3249
3250                    if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]:
3251                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format(
3252                            targetPrice, instrument["currency"],
3253                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3254                        ))
3255
3256            else:
3257                uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log and try again open order later.")
3258
3259        if orderType == "Stop":
3260            uLogger.debug(
3261                "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format(
3262                    self._ticker, self._figi,
3263                    operation, lots,
3264                    targetPrice, instrument["currency"],
3265                    limitPrice, instrument["currency"],
3266                    stopType, expDate,
3267                ))
3268
3269            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder"
3270            expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT)
3271            stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT"
3272
3273            body = {
3274                "figi": self._figi,
3275                "quantity": str(lots),
3276                "price": FloatToNano(limitPrice),
3277                "stopPrice": FloatToNano(targetPrice),
3278                "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL",  # see: TKS_STOP_ORDER_DIRECTIONS
3279                "accountId": str(self.accountId),
3280                "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL",  # see: TKS_STOP_ORDER_EXPIRATION_TYPES
3281                "stopOrderType": stopOrderType,  # see: TKS_STOP_ORDER_TYPES
3282            }
3283
3284            if expDateUTC:
3285                body["expireDate"] = expDateUTC
3286
3287            self.body = str(body)
3288            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0)
3289
3290            if "stopOrderId" in response.keys():
3291                uLogger.info(
3292                    "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{} {}], limit price [{} {}], stop-order type [{}] and expiration date [{} UTC]".format(
3293                        response["stopOrderId"], self._ticker, self._figi, operation, lots,
3294                        "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"],
3295                        "{:.4f}".format(limitPrice).rstrip("0").rstrip("."), instrument["currency"],
3296                        TKS_STOP_ORDER_TYPES[stopOrderType],
3297                        datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"],
3298                    ))
3299
3300                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3301                    if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3302                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3303                            targetPrice, instrument["currency"],
3304                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3305                        ))
3306
3307                    if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3308                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3309                            targetPrice, instrument["currency"],
3310                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3311                        ))
3312
3313            else:
3314                uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log and try again open order later.")
3315
3316        return response
3317
3318    def BuyLimit(self, lots: int, targetPrice: float) -> dict:
3319        """
3320        Create pending `Buy` limit-order (below current price). You must specify only 2 parameters:
3321        `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then
3322        broker immediately open `Buy` market order, such as if you do simple `--buy` operation!
3323        See also: `Order()` docstring.
3324
3325        :param lots: volume, integer count of lots >= 1.
3326        :param targetPrice: target price > 0. This is open trade price for limit order.
3327        :return: JSON with response from broker server.
3328        """
3329        return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice)
3330
3331    def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3332        """
3333        Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order.
3334        In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3335        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3336        target price value then broker opens a limit order. See also: `Order()` docstring.
3337
3338        :param lots: volume, integer count of lots >= 1.
3339        :param targetPrice: target price > 0. This is trigger price for buy stop-order.
3340        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3341                           with price equal to limitPrice, when current price goes to target price of buy stop-order.
3342        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3343                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3344        :param expDate: string "Undefined" by default or local date in future.
3345                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3346                        This date is converting to UTC format for server.
3347        :return: JSON with response from broker server.
3348        """
3349        return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
3350
3351    def SellLimit(self, lots: int, targetPrice: float) -> dict:
3352        """
3353        Create pending `Sell` limit-order (above current price). You must specify only 2 parameters:
3354        `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then
3355        broker immediately open `Sell` market order, such as if you do simple `--sell` operation!
3356        See also: `Order()` docstring.
3357
3358        :param lots: volume, integer count of lots >= 1.
3359        :param targetPrice: target price > 0. This is open trade price for limit order.
3360        :return: JSON with response from broker server.
3361        """
3362        return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice)
3363
3364    def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3365        """
3366        Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order.
3367        In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3368        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3369        target price value then broker opens a limit order. See also: `Order()` docstring.
3370
3371        :param lots: volume, integer count of lots >= 1.
3372        :param targetPrice: target price > 0. This is trigger price for sell stop-order.
3373        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3374                           with price equal to limitPrice, when current price goes to target price of sell stop-order.
3375        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3376                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3377        :param expDate: string "Undefined" by default or local date in future.
3378                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3379                        This date is converting to UTC format for server.
3380        :return: JSON with response from broker server.
3381        """
3382        return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
3383
3384    def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None:
3385        """
3386        Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`.
3387
3388        :param orderIDs: list of integers with `orderId` or `stopOrderId`.
3389        :param allOrdersIDs: pre-received lists of all active pending limit orders.
3390                             This avoids unnecessary downloading data from the server.
3391        :param allStopOrdersIDs: pre-received lists of all active stop orders.
3392        """
3393        if self.accountId is None or not self.accountId:
3394            uLogger.error("Variable `accountId` must be defined for using this method!")
3395            raise Exception("Account ID required")
3396
3397        if orderIDs:
3398            if allOrdersIDs is None:
3399                rawOrders = self.RequestPendingOrders()
3400                allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending limit orders ID
3401
3402            if allStopOrdersIDs is None:
3403                rawStopOrders = self.RequestStopOrders()
3404                allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3405
3406            for orderID in orderIDs:
3407                idInPendingOrders = orderID in allOrdersIDs
3408                idInStopOrders = orderID in allStopOrdersIDs
3409
3410                if not (idInPendingOrders or idInStopOrders):
3411                    uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID))
3412                    continue
3413
3414                else:
3415                    if idInPendingOrders:
3416                        uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID))
3417
3418                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder
3419                        self.body = str({"accountId": self.accountId, "orderId": orderID})
3420                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder"
3421                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3422
3423                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3424                            if self.moreDebug:
3425                                uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3426
3427                            uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID))
3428
3429                        else:
3430                            uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID))
3431
3432                    elif idInStopOrders:
3433                        uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID))
3434
3435                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder
3436                        self.body = str({"accountId": self.accountId, "stopOrderId": orderID})
3437                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder"
3438                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3439
3440                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3441                            if self.moreDebug:
3442                                uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3443
3444                            uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID))
3445
3446                        else:
3447                            uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID))
3448
3449                    else:
3450                        continue
3451
3452    def CloseAllOrders(self) -> None:
3453        """
3454        Gets a list of open pending and stop orders and cancel it all.
3455        """
3456        rawOrders = self.RequestPendingOrders()
3457        allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending limit orders ID
3458        lenOrders = len(allOrdersIDs)
3459
3460        rawStopOrders = self.RequestStopOrders()
3461        allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3462        lenSOrders = len(allStopOrdersIDs)
3463
3464        if lenOrders > 0 or lenSOrders > 0:
3465            uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders))
3466
3467            self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs)
3468
3469        else:
3470            uLogger.info("Orders not found, nothing to cancel.")
3471
3472    def CloseAll(self, *args) -> None:
3473        """
3474        Close all available (not blocked) opened trades and orders.
3475
3476        Also, you can select one or more keywords case-insensitive:
3477        `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type.
3478
3479        Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods.
3480        """
3481        overview = self.Overview(show=False)  # get all open trades info
3482
3483        if len(args) == 0:
3484            uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...")
3485            self.CloseAllOrders()  # close all pending and stop orders
3486
3487            for iType in TKS_INSTRUMENTS:
3488                if iType != "Currencies":
3489                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies
3490
3491        else:
3492            uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args)))
3493            lowerArgs = [x.lower() for x in args]
3494
3495            if "orders" in lowerArgs:
3496                self.CloseAllOrders()  # close all pending and stop orders
3497
3498            for iType in TKS_INSTRUMENTS:
3499                if iType.lower() in lowerArgs and iType != "Currencies":
3500                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies
3501
3502    def CloseAllByTicker(self, instrument: str) -> None:
3503        """
3504        Close all available (not blocked) opened trades and orders for one instrument defined by its ticker.
3505
3506        This method searches opened trade and orders of instrument throw all portfolio and then use
3507        `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument.
3508
3509        See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`.
3510
3511        :param instrument: string with ticker.
3512        """
3513        if instrument is None or not instrument:
3514            uLogger.error("Ticker name must be defined for using this method!")
3515            raise Exception("Ticker required")
3516
3517        overview = self.Overview(show=False)  # get user portfolio with all open trades info
3518
3519        self._ticker = instrument  # try to set instrument as ticker
3520        self._figi = ""
3521
3522        limitAll = [item["orderID"] for item in overview["stat"]["orders"]]  # list of all pending limit order IDs
3523        stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]]  # list of all stop order IDs
3524
3525        if limitAll and self.IsInLimitOrders(portfolio=overview):
3526            uLogger.debug("Closing all opened pending limit orders for the instrument with ticker [{}]. Wait, please...")
3527            self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3528
3529        if stopAll and self.IsInStopOrders(portfolio=overview):
3530            uLogger.debug("Closing all opened stop orders for the instrument with ticker [{}]. Wait, please...")
3531            self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3532
3533        if self.IsInPortfolio(portfolio=overview):
3534            uLogger.debug("Closing all available (not blocked) opened trade for the instrument with ticker [{}]. Wait, please...")
3535            self.CloseTrades(instruments=[instrument], portfolio=overview)
3536
3537    def CloseAllByFIGI(self, instrument: str) -> None:
3538        """
3539        Close all available (not blocked) opened trades and orders for one instrument defined by its FIGI id.
3540
3541        This method searches opened trade and orders of instrument throw all portfolio and then use
3542        `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument.
3543
3544        See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`.
3545
3546        :param instrument: string with FIGI id.
3547        """
3548        if instrument is None or not instrument:
3549            uLogger.error("FIGI id must be defined for using this method!")
3550            raise Exception("FIGI required")
3551
3552        overview = self.Overview(show=False)  # get user portfolio with all open trades info
3553
3554        self._ticker = ""
3555        self._figi = instrument  # try to set instrument as FIGI id
3556
3557        limitAll = [item["orderID"] for item in overview["stat"]["orders"]]  # list of all pending limit order IDs
3558        stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]]  # list of all stop order IDs
3559
3560        if limitAll and self.IsInLimitOrders(portfolio=overview):
3561            uLogger.debug("Closing all opened pending limit orders for the instrument with FIGI [{}]. Wait, please...")
3562            self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3563
3564        if stopAll and self.IsInStopOrders(portfolio=overview):
3565            uLogger.debug("Closing all opened stop orders for the instrument with FIGI [{}]. Wait, please...")
3566            self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3567
3568        if self.IsInPortfolio(portfolio=overview):
3569            uLogger.debug("Closing all available (not blocked) opened trade for the instrument with FIGI [{}]. Wait, please...")
3570            self.CloseTrades(instruments=[instrument], portfolio=overview)
3571
3572    @staticmethod
3573    def ParseOrderParameters(operation, **inputParameters):
3574        """
3575        Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.
3576
3577        :param operation: string "Buy" or "Sell".
3578        :param inputParameters: this is dict of strings that looks like this
3579               `{"lots": "L_int,...", "prices": "P_float,..."}` where
3580               "lots" key: one or more lot values (integer numbers) to open with every limit-order
3581               "prices" key: one or more prices to open limit-orders
3582               Counts of values in lots and prices lists must be equals!
3583        :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]`
3584        """
3585        # TODO: update order grid work with api v2
3586        pass
3587        # uLogger.debug("Input parameters: {}".format(inputParameters))
3588        #
3589        # if operation is None or not operation or operation not in ("Buy", "Sell"):
3590        #     uLogger.error("You must define operation type: 'Buy' or 'Sell'!")
3591        #     raise Exception("Incorrect value")
3592        #
3593        # if "l" in inputParameters.keys():
3594        #     inputParameters["lots"] = inputParameters.pop("l")
3595        #
3596        # if "p" in inputParameters.keys():
3597        #     inputParameters["prices"] = inputParameters.pop("p")
3598        #
3599        # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys():
3600        #     uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!")
3601        #     raise Exception("Incorrect value")
3602        #
3603        # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")]
3604        # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")]
3605        #
3606        # if len(lots) != len(prices):
3607        #     uLogger.error("'lots' and 'prices' lists must have equal length of values!")
3608        #     raise Exception("Incorrect value")
3609        #
3610        # uLogger.debug("Extracted parameters for orders:")
3611        # uLogger.debug("lots = {}".format(lots))
3612        # uLogger.debug("prices = {}".format(prices))
3613        #
3614        # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...]
3615        # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))]
3616        # uLogger.debug("Order parameters: {}".format(result))
3617        #
3618        # return result
3619
3620    def IsInPortfolio(self, portfolio: dict = None) -> bool:
3621        """
3622        Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`.
3623
3624        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3625        :return: `True` if portfolio contains open position with given instrument, `False` otherwise.
3626        """
3627        result = False
3628        msg = "Instrument not defined!"
3629
3630        if portfolio is None or not portfolio:
3631            portfolio = self.Overview(show=False)
3632
3633        if self._ticker:
3634            uLogger.debug("Searching instrument with ticker [{}] throw opened positions list...".format(self._ticker))
3635            msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker)
3636
3637            for iType in TKS_INSTRUMENTS:
3638                for instrument in portfolio["stat"][iType]:
3639                    if instrument["ticker"] == self._ticker:
3640                        result = True
3641                        msg = "Instrument with ticker [{}] is present in open positions".format(self._ticker)
3642                        break
3643
3644        elif self._figi:
3645            uLogger.debug("Searching instrument with FIGI [{}] throw opened positions list...".format(self._figi))
3646            msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi)
3647
3648            for iType in TKS_INSTRUMENTS:
3649                for instrument in portfolio["stat"][iType]:
3650                    if instrument["figi"] == self._figi:
3651                        result = True
3652                        msg = "Instrument with FIGI [{}] is present in open positions".format(self._figi)
3653                        break
3654
3655        else:
3656            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3657
3658        uLogger.debug(msg)
3659
3660        return result
3661
3662    def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict:
3663        """
3664        Returns instrument from the user's portfolio if it presents there.
3665        Instrument must be defined by `ticker` (highly priority) or `figi`.
3666
3667        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3668        :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise.
3669        """
3670        result = None
3671        msg = "Instrument not defined!"
3672
3673        if portfolio is None or not portfolio:
3674            portfolio = self.Overview(show=False)
3675
3676        if self._ticker:
3677            uLogger.debug("Searching instrument with ticker [{}] in opened positions...".format(self._ticker))
3678            msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker)
3679
3680            for iType in TKS_INSTRUMENTS:
3681                for instrument in portfolio["stat"][iType]:
3682                    if instrument["ticker"] == self._ticker:
3683                        result = instrument
3684                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self._ticker, instrument["figi"])
3685                        break
3686
3687        elif self._figi:
3688            uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self._figi))
3689            msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi)
3690
3691            for iType in TKS_INSTRUMENTS:
3692                for instrument in portfolio["stat"][iType]:
3693                    if instrument["figi"] == self._figi:
3694                        result = instrument
3695                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self._figi)
3696                        break
3697
3698        else:
3699            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3700
3701        uLogger.debug(msg)
3702
3703        return result
3704
3705    def IsInLimitOrders(self, portfolio: dict = None) -> bool:
3706        """
3707        Checks if instrument is in the limit orders list. Instrument must be defined by `ticker` (highly priority) or `figi`.
3708
3709        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3710
3711        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3712        :return: `True` if limit orders list contains some limit orders for the instrument, `False` otherwise.
3713        """
3714        result = False
3715        msg = "Instrument not defined!"
3716
3717        if portfolio is None or not portfolio:
3718            portfolio = self.Overview(show=False)
3719
3720        if self._ticker:
3721            uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker))
3722            msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker)
3723
3724            for instrument in portfolio["stat"]["orders"]:
3725                if instrument["ticker"] == self._ticker:
3726                    result = True
3727                    msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker)
3728                    break
3729
3730        elif self._figi:
3731            uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi))
3732            msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi)
3733
3734            for instrument in portfolio["stat"]["orders"]:
3735                if instrument["figi"] == self._figi:
3736                    result = True
3737                    msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi)
3738                    break
3739
3740        else:
3741            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3742
3743        uLogger.debug(msg)
3744
3745        return result
3746
3747    def GetLimitOrderIDs(self, portfolio: dict = None) -> list[str]:
3748        """
3749        Returns list with all `orderID`s of opened pending limit orders for the instrument.
3750        Instrument must be defined by `ticker` (highly priority) or `figi`.
3751
3752        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3753
3754        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3755        :return: list with `orderID`s of limit orders.
3756        """
3757        result = []
3758        msg = "Instrument not defined!"
3759
3760        if portfolio is None or not portfolio:
3761            portfolio = self.Overview(show=False)
3762
3763        if self._ticker:
3764            uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker))
3765            msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker)
3766
3767            for instrument in portfolio["stat"]["orders"]:
3768                if instrument["ticker"] == self._ticker:
3769                    result.append(instrument["orderID"])
3770
3771            if result:
3772                msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker)
3773
3774        elif self._figi:
3775            uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi))
3776            msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi)
3777
3778            for instrument in portfolio["stat"]["orders"]:
3779                if instrument["figi"] == self._figi:
3780                    result.append(instrument["orderID"])
3781
3782            if result:
3783                msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi)
3784
3785        else:
3786            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3787
3788        uLogger.debug(msg)
3789
3790        return result
3791
3792    def IsInStopOrders(self, portfolio: dict = None) -> bool:
3793        """
3794        Checks if instrument is in the stop orders list. Instrument must be defined by `ticker` (highly priority) or `figi`.
3795
3796        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3797
3798        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3799        :return: `True` if stop orders list contains some stop orders for the instrument, `False` otherwise.
3800        """
3801        result = False
3802        msg = "Instrument not defined!"
3803
3804        if portfolio is None or not portfolio:
3805            portfolio = self.Overview(show=False)
3806
3807        if self._ticker:
3808            uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker))
3809            msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker)
3810
3811            for instrument in portfolio["stat"]["stopOrders"]:
3812                if instrument["ticker"] == self._ticker:
3813                    result = True
3814                    msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker)
3815                    break
3816
3817        elif self._figi:
3818            uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi))
3819            msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi)
3820
3821            for instrument in portfolio["stat"]["stopOrders"]:
3822                if instrument["figi"] == self._figi:
3823                    result = True
3824                    msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi)
3825                    break
3826
3827        else:
3828            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3829
3830        uLogger.debug(msg)
3831
3832        return result
3833
3834    def GetStopOrderIDs(self, portfolio: dict = None) -> list[str]:
3835        """
3836        Returns list with all `orderID`s of opened stop orders for the instrument.
3837        Instrument must be defined by `ticker` (highly priority) or `figi`.
3838
3839        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3840
3841        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3842        :return: list with `orderID`s of stop orders.
3843        """
3844        result = []
3845        msg = "Instrument not defined!"
3846
3847        if portfolio is None or not portfolio:
3848            portfolio = self.Overview(show=False)
3849
3850        if self._ticker:
3851            uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker))
3852            msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker)
3853
3854            for instrument in portfolio["stat"]["stopOrders"]:
3855                if instrument["ticker"] == self._ticker:
3856                    result.append(instrument["orderID"])
3857
3858            if result:
3859                msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker)
3860
3861        elif self._figi:
3862            uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi))
3863            msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi)
3864
3865            for instrument in portfolio["stat"]["stopOrders"]:
3866                if instrument["figi"] == self._figi:
3867                    result.append(instrument["orderID"])
3868
3869            if result:
3870                msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi)
3871
3872        else:
3873            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3874
3875        uLogger.debug(msg)
3876
3877        return result
3878
3879    def RequestLimits(self) -> dict:
3880        """
3881        Method for obtaining the available funds for withdrawal for current `accountId`.
3882
3883        See also:
3884        - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits
3885        - `OverviewLimits()` method
3886
3887        :return: dict with raw data from server that contains free funds for withdrawal. Example of dict:
3888                 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`.
3889                 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency
3890                 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures.
3891        """
3892        if self.accountId is None or not self.accountId:
3893            uLogger.error("Variable `accountId` must be defined for using this method!")
3894            raise Exception("Account ID required")
3895
3896        uLogger.debug("Requesting current available funds for withdrawal. Wait, please...")
3897
3898        self.body = str({"accountId": self.accountId})
3899        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits"
3900        rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
3901
3902        if self.moreDebug:
3903            uLogger.debug("Records about available funds for withdrawal successfully received")
3904
3905        return rawLimits
3906
3907    def OverviewLimits(self, show: bool = False) -> dict:
3908        """
3909        Method for parsing and show table with available funds for withdrawal for current `accountId`.
3910
3911        See also: `RequestLimits()`.
3912
3913        :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log.
3914        :return: dict with raw parsed data from server and some calculated statistics about it.
3915        """
3916        if self.accountId is None or not self.accountId:
3917            uLogger.error("Variable `accountId` must be defined for using this method!")
3918            raise Exception("Account ID required")
3919
3920        rawLimits = self.RequestLimits()  # raw response with current available funds for withdrawal
3921
3922        view = {
3923            "rawLimits": rawLimits,
3924            "limits": {  # parsed data for every currency:
3925                "money": {  # this is an array of portfolio currency positions
3926                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"]
3927                },
3928                "blocked": {  # this is an array of blocked currency
3929                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"]
3930                },
3931                "blockedGuarantee": {  # this is locked money under collateral for futures
3932                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"]
3933                },
3934            },
3935        }
3936
3937        # --- Prepare text table with limits in human-readable format:
3938        if show:
3939            info = [
3940                "# Withdrawal limits\n\n",
3941                "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
3942                "* **Account ID:** [{}]\n".format(self.accountId),
3943            ]
3944
3945            if view["limits"]["money"]:
3946                info.extend([
3947                    "\n| Currencies | Total         | Available for withdrawal | Blocked for trade | Futures guarantee |\n",
3948                    "|------------|---------------|--------------------------|-------------------|-------------------|\n",
3949                ])
3950
3951            else:
3952                info.append("\nNo withdrawal limits\n")
3953
3954            for curr in view["limits"]["money"].keys():
3955                blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0
3956                blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0
3957                availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee)
3958
3959                infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format(
3960                    "[{}]".format(curr),
3961                    "{:.2f}".format(view["limits"]["money"][curr]),
3962                    "{:.2f}".format(availableMoney),
3963                    "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—",
3964                    "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—",
3965                )
3966
3967                if curr == "rub":
3968                    info.insert(5, infoStr)  # hack: insert "rub" at the first position in table and after headers
3969
3970                else:
3971                    info.append(infoStr)
3972
3973            infoText = "".join(info)
3974
3975            uLogger.info(infoText)
3976
3977            if self.withdrawalLimitsFile:
3978                with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH:
3979                    fH.write(infoText)
3980
3981                uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile)))
3982
3983                if self.useHTMLReports:
3984                    htmlFilePath = self.withdrawalLimitsFile.replace(".md", ".html") if self.withdrawalLimitsFile.endswith(".md") else self.withdrawalLimitsFile + ".html"
3985                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
3986                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Withdrawal limits", commonCSS=COMMON_CSS, markdown=infoText))
3987
3988                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
3989
3990        return view
3991
3992    def RequestAccounts(self) -> dict:
3993        """
3994        Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`.
3995
3996        See also:
3997        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts
3998        - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account
3999        - `OverviewUserInfo()` method
4000
4001        :return: dict with raw data from server that contains accounts info. Example of dict:
4002                 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account",
4003                   "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z",
4004                   "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`.
4005                 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now.
4006        """
4007        uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...")
4008
4009        self.body = str({})
4010        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts"
4011        rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST")
4012
4013        if self.moreDebug:
4014            uLogger.debug("Records about available accounts successfully received")
4015
4016        return rawAccounts
4017
4018    def RequestUserInfo(self) -> dict:
4019        """
4020        Method for requesting common user's information.
4021
4022        See also:
4023        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo
4024        - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest
4025        - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with
4026        - `OverviewUserInfo()` method
4027
4028        :return: dict with raw data from server that contains user's information. Example of dict:
4029                 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage",
4030                   "russian_shares", "structured_income_bonds"], "tariff": "premium"}`.
4031        """
4032        uLogger.debug("Requesting common user's information. Wait, please...")
4033
4034        self.body = str({})
4035        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo"
4036        rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST")
4037
4038        if self.moreDebug:
4039            uLogger.debug("Records about current user successfully received")
4040
4041        return rawUserInfo
4042
4043    def RequestMarginStatus(self, accountId: str = None) -> dict:
4044        """
4045        Method for requesting margin calculation for defined account ID.
4046
4047        See also:
4048        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes
4049        - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse
4050        - `OverviewUserInfo()` method
4051
4052        :param accountId: string with numeric account ID. If `None`, then used class field `accountId`.
4053        :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict.
4054                 Example of responses:
4055                 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`.
4056                 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000},
4057                                    "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000},
4058                                    "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000},
4059                                    "fundsSufficiencyLevel": {"units": "1", "nano": 280000000},
4060                                    "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`.
4061        """
4062        if accountId is None or not accountId:
4063            if self.accountId is None or not self.accountId:
4064                uLogger.error("Variable `accountId` must be defined for using this method!")
4065                raise Exception("Account ID required")
4066
4067            else:
4068                accountId = self.accountId  # use `self.accountId` (main ID) by default
4069
4070        uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId))
4071
4072        self.body = str({"accountId": accountId})
4073        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes"
4074        rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST")
4075
4076        if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}:
4077            uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId))
4078            rawMargin = {}
4079
4080        else:
4081            if self.moreDebug:
4082                uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId))
4083
4084        return rawMargin
4085
4086    def RequestTariffLimits(self) -> dict:
4087        """
4088        Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`.
4089
4090        See also:
4091        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff
4092        - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest
4093        - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit
4094        - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit
4095        - `OverviewUserInfo()` method
4096
4097        :return: dict with raw data from server that contains limits of current tariff. Example of dict:
4098                 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...],
4099                   "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`.
4100        """
4101        uLogger.debug("Requesting limits of current tariff. Wait, please...")
4102
4103        self.body = str({})
4104        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff"
4105        rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
4106
4107        if self.moreDebug:
4108            uLogger.debug("Records with limits of current tariff successfully received")
4109
4110        return rawTariffLimits
4111
4112    def RequestBondCoupons(self, iJSON: dict) -> dict:
4113        """
4114        Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown
4115        then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`.
4116        All dates are in UTC timezone.
4117
4118        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons
4119        Documentation:
4120        - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest
4121        - response: https://tinkoff.github.io/investAPI/instruments/#coupon
4122
4123        See also: `ExtendBondsData()`.
4124
4125        :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self._ticker]`
4126                      If raw iJSON is not data of bond then server returns an error [400] with message:
4127                      `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`.
4128        :return: dictionary with bond payment calendar. Response example
4129                 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12",
4130                   "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000},
4131                   "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z",
4132                   "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}`
4133        """
4134        if iJSON["figi"] is None or not iJSON["figi"]:
4135            uLogger.error("FIGI must be defined for using this method!")
4136            raise Exception("FIGI required")
4137
4138        startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z"
4139        endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z"
4140
4141        uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format(
4142            "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "",
4143            self._figi,
4144            startDate,
4145            endDate,
4146        ))
4147
4148        self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate})
4149        calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons"
4150        calendar = self.SendAPIRequest(calendarURL, reqType="POST")
4151
4152        if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}:
4153            uLogger.warning("Instrument type is not bond!")
4154
4155        else:
4156            if self.moreDebug:
4157                uLogger.debug("Records about bond payment calendar successfully received")
4158
4159        return calendar
4160
4161    def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame:
4162        """
4163        Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider
4164        Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar,
4165        coupon yields, current yields and some statistics etc.
4166
4167        WARNING! This is too long operation if a lot of bonds requested from broker server.
4168
4169        See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`.
4170
4171        :param instruments: list of strings with tickers or FIGIs.
4172        :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`,
4173                     for further used by data scientists or stock analytics.
4174        :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker.
4175                 In XLSX-file and Pandas DataFrame fields mean:
4176                 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond
4177                 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon
4178        """
4179        if instruments is None or not instruments:
4180            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
4181            raise Exception("Ticker or FIGI required")
4182
4183        if isinstance(instruments, str):
4184            instruments = [instruments]
4185
4186        uniqueInstruments = self.GetUniqueFIGIs(instruments)
4187
4188        uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...")
4189
4190        iCount = len(uniqueInstruments)
4191        tooLong = iCount >= 20
4192        if tooLong:
4193            uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...")
4194
4195        bonds = None
4196        for i, self._figi in enumerate(uniqueInstruments):
4197            instrument = self.SearchByFIGI(requestPrice=False)  # raw data about instrument from server
4198
4199            if "type" in instrument.keys() and instrument["type"] == "Bonds":
4200                # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond
4201                rawBond = self.SearchByFIGI(requestPrice=True)
4202
4203                # Widen raw data with UTC current time (iData["actualDateTime"]):
4204                actualDate = datetime.now(tzutc())
4205                iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond
4206
4207                # Widen raw data with bond payment calendar (iData["rawCalendar"]):
4208                iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)}
4209
4210                # Replace some values with human-readable:
4211                iData["nominalCurrency"] = iData["nominal"]["currency"]
4212                iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"])
4213                iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"])
4214                iData["aciCurrency"] = iData["aciValue"]["currency"]
4215                iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"])
4216                iData["issueSize"] = int(iData["issueSize"])
4217                iData["issueSizePlan"] = int(iData["issueSizePlan"])
4218                iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]]
4219                iData["step"] = iData["step"] if "step" in iData.keys() else 0
4220                iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]]
4221                iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0
4222                iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0
4223                iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0
4224                iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0
4225                iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0
4226                iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0
4227
4228                # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date):
4229                iData["limitUpPercent"] = iData["currentPrice"]["limitUp"]  # max price on current day in percents of nominal
4230                iData["limitDownPercent"] = iData["currentPrice"]["limitDown"]  # min price on current day in percents of nominal
4231                iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"]  # last price on market in percents of nominal
4232                iData["closePricePercent"] = iData["currentPrice"]["closePrice"]  # previous day close in percents of nominal
4233                iData["changes"] = iData["currentPrice"]["changes"]  # this is percent of changes between `currentPrice` and `lastPrice`
4234                iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100  # max price on current day is `limitUpPercent` * `nominal`
4235                iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100  # min price on current day is `limitDownPercent` * `nominal`
4236                iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100  # last price on market is `lastPricePercent` * `nominal`
4237                iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100  # previous day close is `closePricePercent` * `nominal`
4238                iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"]  # this is delta between last deal price and last close
4239
4240                # Widen raw data with calendar data from `rawCalendar` values:
4241                calendarData = []
4242                if "events" in iData["rawCalendar"].keys():
4243                    for item in iData["rawCalendar"]["events"]:
4244                        calendarData.append({
4245                            "couponDate": item["couponDate"],
4246                            "couponNumber": int(item["couponNumber"]),
4247                            "fixDate": item["fixDate"] if "fixDate" in item.keys() else "",
4248                            "payCurrency": item["payOneBond"]["currency"],
4249                            "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]),
4250                            "couponType": TKS_COUPON_TYPES[item["couponType"]],
4251                            "couponStartDate": item["couponStartDate"],
4252                            "couponEndDate": item["couponEndDate"],
4253                            "couponPeriod": item["couponPeriod"],
4254                        })
4255
4256                    # if maturity date is unknown then uses the latest date in bond payment calendar for it:
4257                    if "maturityDate" not in iData.keys():
4258                        iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else ""
4259
4260                # Widen raw data with Coupon Rate.
4261                # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%:
4262                iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData])
4263                iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData])
4264                iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0.
4265
4266                # Widen raw data with Yield to Maturity (YTM) on current date.
4267                # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%:
4268                maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None
4269                iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None
4270                iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate])
4271                iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"]  # sum of all last coupons minus current ACI value
4272                iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0.
4273
4274                iData["calendar"] = calendarData  # adds calendar at the end
4275
4276                # Remove not used data:
4277                iData.pop("uid")
4278                iData.pop("positionUid")
4279                iData.pop("currentPrice")
4280                iData.pop("rawCalendar")
4281
4282                colNames = list(iData.keys())
4283                if bonds is None:
4284                    bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames))
4285
4286                else:
4287                    bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True)
4288
4289            else:
4290                uLogger.warning("Instrument is not a bond!")
4291
4292            processed = round(100 * (i + 1) / iCount, 1)
4293            if tooLong and processed % 5 == 0:
4294                uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount))
4295
4296            else:
4297                uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount))
4298
4299        bonds.index = bonds["ticker"].tolist()  # replace indexes with ticker names
4300
4301        # Saving bonds from Pandas DataFrame to XLSX sheet:
4302        if xlsx and self.bondsXLSXFile:
4303            with pd.ExcelWriter(
4304                    path=self.bondsXLSXFile,
4305                    date_format=TKS_DATE_FORMAT,
4306                    datetime_format=TKS_DATE_TIME_FORMAT,
4307                    mode="w",
4308            ) as writer:
4309                bonds.to_excel(
4310                    writer,
4311                    sheet_name="Extended bonds data",
4312                    index=True,
4313                    encoding="UTF-8",
4314                    freeze_panes=(1, 1),
4315                )  # saving as XLSX-file with freeze first row and column as headers
4316
4317            uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile)))
4318
4319        return bonds
4320
4321    def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame:
4322        """
4323        Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default.
4324
4325        WARNING! This is too long operation if a lot of bonds requested from broker server.
4326
4327        See also: `ShowBondsCalendar()`, `ExtendBondsData()`.
4328
4329        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
4330                        extended information about bonds: main info, current prices, bond payment calendar,
4331                        coupon yields, current yields and some statistics etc.
4332                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4333        :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default,
4334                     for further used by data scientists or stock analytics.
4335        :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon
4336        """
4337        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4338            extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=False)
4339
4340        uLogger.debug("Generating bond payments calendar data. Wait, please...")
4341
4342        colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"]
4343        colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"]
4344        calendar = None
4345        for bond in extBonds.iterrows():
4346            for item in bond[1]["calendar"]:
4347                cData = {
4348                    "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()),
4349                    "couponDate": item["couponDate"],
4350                    "figi": bond[1]["figi"],
4351                    "ticker": bond[1]["ticker"],
4352                    "name": bond[1]["name"],
4353                    "couponNumber": item["couponNumber"],
4354                    "payOneBond": item["payOneBond"],
4355                    "payCurrency": item["payCurrency"],
4356                    "couponType": item["couponType"],
4357                    "couponPeriod": item["couponPeriod"],
4358                    "fixDate": item["fixDate"],
4359                    "couponStartDate": item["couponStartDate"],
4360                    "couponEndDate": item["couponEndDate"],
4361                }
4362
4363                if calendar is None:
4364                    calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID))
4365
4366                else:
4367                    calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True)
4368
4369        if calendar is not None:
4370            calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True)  # sort all payments for all bonds by payment date
4371
4372            # Saving calendar from Pandas DataFrame to XLSX sheet:
4373            if xlsx:
4374                xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx"
4375
4376                with pd.ExcelWriter(
4377                        path=xlsxCalendarFile,
4378                        date_format=TKS_DATE_FORMAT,
4379                        datetime_format=TKS_DATE_TIME_FORMAT,
4380                        mode="w",
4381                ) as writer:
4382                    humanReadable = calendar.copy(deep=True)
4383                    humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0])
4384                    humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0])
4385                    humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0])
4386                    humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0])
4387                    humanReadable.columns = colNames  # human-readable column names
4388
4389                    humanReadable.to_excel(
4390                        writer,
4391                        sheet_name="Bond payments calendar",
4392                        index=False,
4393                        encoding="UTF-8",
4394                        freeze_panes=(1, 2),
4395                    )  # saving as XLSX-file with freeze first row and column as headers
4396
4397                    del humanReadable  # release df in memory
4398
4399                uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile)))
4400
4401        return calendar
4402
4403    def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True) -> str:
4404        """
4405        Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond.
4406        Also, creates Markdown file with calendar data, `calendar.md` by default.
4407
4408        See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`.
4409
4410        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
4411                        extended information about bonds: main info, current prices, bond payment calendar,
4412                        coupon yields, current yields and some statistics etc.
4413                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4414        :param show: if `True` then also printing bonds payment calendar to the console,
4415                     otherwise save to file `calendarFile` only. `False` by default.
4416        :return: multilines text in Markdown format with bonds payment calendar as a table.
4417        """
4418        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4419            extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=False)
4420
4421        infoText = "# Bond payments calendar\n\n"
4422
4423        calendar = self.CreateBondsCalendar(extBonds, xlsx=True)  # generate Pandas DataFrame with full calendar data
4424
4425        if not (calendar is None or calendar.empty):
4426            splitLine = "|       |                 |              |              |     |               |           |        |                   |\n"
4427
4428            info = [
4429                "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4430                "| Paid  | Payment date    | FIGI         | Ticker       | No. | Value         | Type      | Period | End registry date |\n",
4431                "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n",
4432            ]
4433
4434            newMonth = False
4435            notOneBond = calendar["figi"].nunique() > 1
4436            for i, bond in enumerate(calendar.iterrows()):
4437                if newMonth and notOneBond:
4438                    info.append(splitLine)
4439
4440                info.append(
4441                    "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format(
4442                        "  √" if bond[1]["paid"] else "  —",
4443                        bond[1]["couponDate"].split("T")[0],
4444                        bond[1]["figi"],
4445                        bond[1]["ticker"],
4446                        bond[1]["couponNumber"],
4447                        "{} {}".format(
4448                            "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."),
4449                            bond[1]["payCurrency"],
4450                        ),
4451                        bond[1]["couponType"],
4452                        bond[1]["couponPeriod"],
4453                        bond[1]["fixDate"].split("T")[0],
4454                    )
4455                )
4456
4457                if i < len(calendar.values) - 1:
4458                    curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4459                    nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4460                    newMonth = False if curDate.month == nextDate.month else True
4461
4462                else:
4463                    newMonth = False
4464
4465            infoText += "".join(info)
4466
4467            if show:
4468                uLogger.info("{}".format(infoText))
4469
4470            if self.calendarFile is not None:
4471                with open(self.calendarFile, "w", encoding="UTF-8") as fH:
4472                    fH.write(infoText)
4473
4474                uLogger.info("Bond payments calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile)))
4475
4476                if self.useHTMLReports:
4477                    htmlFilePath = self.calendarFile.replace(".md", ".html") if self.calendarFile.endswith(".md") else self.calendarFile + ".html"
4478                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
4479                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Bond payments calendar", commonCSS=COMMON_CSS, markdown=infoText))
4480
4481                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
4482
4483        else:
4484            infoText += "No data\n"
4485
4486        return infoText
4487
4488    def OverviewAccounts(self, show: bool = False) -> dict:
4489        """
4490        Method for parsing and show simple table with all available user accounts.
4491
4492        See also: `RequestAccounts()` and `OverviewUserInfo()` methods.
4493
4494        :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log.
4495        :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict:
4496                 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...},
4497                          "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1",
4498                                                        "status": "Opened and active account", "opened": "2018-05-23 00:00:00",
4499                                                        "closed": "—", "access": "Full access" }, ...}}`
4500        """
4501        rawAccounts = self.RequestAccounts()  # Raw responses with accounts
4502
4503        # This is an array of dict with user accounts, its `accountId`s and some parsed data:
4504        accounts = {
4505            item["id"]: {
4506                "type": TKS_ACCOUNT_TYPES[item["type"]],
4507                "name": item["name"],
4508                "status": TKS_ACCOUNT_STATUSES[item["status"]],
4509                "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4510                "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—",
4511                "access": TKS_ACCESS_LEVELS[item["accessLevel"]],
4512            } for item in rawAccounts["accounts"]
4513        }
4514
4515        # Raw and parsed data with some fields replaced in "stat" section:
4516        view = {
4517            "rawAccounts": rawAccounts,
4518            "stat": accounts,
4519        }
4520
4521        # --- Prepare simple text table with only accounts data in human-readable format:
4522        if show:
4523            info = [
4524                "# User accounts\n\n",
4525                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4526                "| Account ID   | Type                      | Status                    | Name                           |\n",
4527                "|--------------|---------------------------|---------------------------|--------------------------------|\n",
4528            ]
4529
4530            for account in view["stat"].keys():
4531                info.extend([
4532                    "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format(
4533                        account,
4534                        view["stat"][account]["type"],
4535                        view["stat"][account]["status"],
4536                        view["stat"][account]["name"],
4537                    )
4538                ])
4539
4540            infoText = "".join(info)
4541
4542            uLogger.info(infoText)
4543
4544            if self.userAccountsFile:
4545                with open(self.userAccountsFile, "w", encoding="UTF-8") as fH:
4546                    fH.write(infoText)
4547
4548                uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile)))
4549
4550                if self.useHTMLReports:
4551                    htmlFilePath = self.userAccountsFile.replace(".md", ".html") if self.userAccountsFile.endswith(".md") else self.userAccountsFile + ".html"
4552                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
4553                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User accounts", commonCSS=COMMON_CSS, markdown=infoText))
4554
4555                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
4556
4557        return view
4558
4559    def OverviewUserInfo(self, show: bool = False) -> dict:
4560        """
4561        Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit).
4562
4563        See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods.
4564
4565        :param show: if `False` then only dictionary returns, if `True` then also print user's data to log.
4566        :return: dict with raw parsed data from server and some calculated statistics about it.
4567        """
4568        overview = self.Overview(show=False)  # Request current user portfolio for the ability to calculate missing funds
4569        tmpTicker = self._ticker
4570        self._ticker = "RUB000UTSTOM"  # This instrument show in rub how much money cost current margin
4571        missing = self.GetInstrumentFromPortfolio(portfolio=overview)
4572        self._ticker = tmpTicker
4573
4574        rawUserInfo = self.RequestUserInfo()  # Raw response with common user info
4575        overviewAccount = self.OverviewAccounts(show=False)  # Raw and parsed accounts data
4576        rawAccounts = overviewAccount["rawAccounts"]  # Raw response with user accounts data
4577        accounts = overviewAccount["stat"]  # Dict with only statistics about user accounts
4578        rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()}  # Raw response with margin calculation for every account ID
4579        rawTariffLimits = self.RequestTariffLimits()  # Raw response with limits of current tariff
4580
4581        # This is dict with parsed common user data:
4582        userInfo = {
4583            "premium": "Yes" if rawUserInfo["premStatus"] else "No",
4584            "qualified": "Yes" if rawUserInfo["qualStatus"] else "No",
4585            "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]],
4586            "tariff": rawUserInfo["tariff"],
4587        }
4588
4589        # This is an array of dict with parsed margin statuses for every account IDs:
4590        margins = {}
4591        for accountId in accounts.keys():
4592            if rawMargins[accountId]:
4593                margins[accountId] = {
4594                    "currency": rawMargins[accountId]["liquidPortfolio"]["currency"],
4595                    "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]),
4596                    "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]),
4597                    "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]),
4598                    "diff": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]),
4599                    "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]),
4600                    "missing": missing["volume"],
4601                }
4602
4603            else:
4604                margins[accountId] = {}  # Server response: margin status is disabled for current accountId
4605
4606        unary = {}  # unary-connection limits
4607        for item in rawTariffLimits["unaryLimits"]:
4608            if item["limitPerMinute"] in unary.keys():
4609                unary[item["limitPerMinute"]].extend(item["methods"])
4610
4611            else:
4612                unary[item["limitPerMinute"]] = item["methods"]
4613
4614        stream = {}  # stream-connection limits
4615        for item in rawTariffLimits["streamLimits"]:
4616            if item["limit"] in stream.keys():
4617                stream[item["limit"]].extend(item["streams"])
4618
4619            else:
4620                stream[item["limit"]] = item["streams"]
4621
4622        # This is dict with parsed limits of current tariff (connections, API methods etc.):
4623        limits = {
4624            "unary": unary,
4625            "stream": stream,
4626        }
4627
4628        # Raw and parsed data as an output result:
4629        view = {
4630            "rawUserInfo": rawUserInfo,
4631            "rawAccounts": rawAccounts,
4632            "rawMargins": rawMargins,
4633            "rawTariffLimits": rawTariffLimits,
4634            "stat": {
4635                "overview": overview,
4636                "userInfo": userInfo,
4637                "accounts": accounts,
4638                "margins": margins,
4639                "limits": limits,
4640            },
4641        }
4642
4643        # --- Prepare text table with user information in human-readable format:
4644        if show:
4645            info = [
4646                "# Full user information\n\n",
4647                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4648                "## Common information\n\n",
4649                "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]),
4650                "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]),
4651                "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]),
4652                "* **Allowed to work with instruments:**\n{}\n".format("".join(["  - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])),
4653                "\n## User accounts\n\n",
4654            ]
4655
4656            for account in view["stat"]["accounts"].keys():
4657                info.extend([
4658                    "### ID: [{}]\n\n".format(account),
4659                    "| Parameters           | Values                                                       |\n",
4660                    "|----------------------|--------------------------------------------------------------|\n",
4661                    "| Account type:        | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]),
4662                    "| Account name:        | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]),
4663                    "| Account status:      | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]),
4664                    "| Access level:        | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]),
4665                    "| Date opened:         | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]),
4666                    "| Date closed:         | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]),
4667                ])
4668
4669                if margins[account]:
4670                    info.extend([
4671                        "| Margin status:       | Enabled                                                      |\n",
4672                        "| - Liquid portfolio:  | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])),
4673                        "| - Margin starting:   | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])),
4674                        "| - Margin minimum:    | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])),
4675                        "| - Margin difference: | {:<60} |\n".format("{} {}".format(margins[account]["diff"], margins[account]["currency"])),
4676                        "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)),
4677                        "| - Not covered funds: | {:<60} |\n\n".format("{:.2f} {}".format(margins[account]["missing"], margins[account]["currency"])),
4678                    ])
4679
4680                else:
4681                    info.append("| Margin status:       | Disabled                                                     |\n\n")
4682
4683            info.extend([
4684                "\n## Current user tariff limits\n",
4685                "\n### See also\n",
4686                "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n",
4687                "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n",
4688                "  - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n",
4689                "  - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n",
4690                "\n### Unary limits\n",
4691            ])
4692
4693            if unary:
4694                for key, values in sorted(unary.items()):
4695                    info.append("\n* Max requests per minute: {}\n".format(key))
4696
4697                    for value in values:
4698                        info.append("  - {}\n".format(value))
4699
4700            else:
4701                info.append("\nNot available\n")
4702
4703            info.append("\n### Stream limits\n")
4704
4705            if stream:
4706                for key, values in sorted(stream.items()):
4707                    info.append("\n* Max stream connections: {}\n".format(key))
4708
4709                    for value in values:
4710                        info.append("  - {}\n".format(value))
4711
4712            else:
4713                info.append("\nNot available\n")
4714
4715            infoText = "".join(info)
4716
4717            uLogger.info(infoText)
4718
4719            if self.userInfoFile:
4720                with open(self.userInfoFile, "w", encoding="UTF-8") as fH:
4721                    fH.write(infoText)
4722
4723                uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile)))
4724
4725                if self.useHTMLReports:
4726                    htmlFilePath = self.userInfoFile.replace(".md", ".html") if self.userInfoFile.endswith(".md") else self.userInfoFile + ".html"
4727                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
4728                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User info", commonCSS=COMMON_CSS, markdown=infoText))
4729
4730                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
4731
4732        return view

This class implements methods to work with Tinkoff broker server.

Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/

About token: https://tinkoff.github.io/investAPI/token/

TinkoffBrokerServer( token: str, accountId: str = None, useCache: bool = True, defaultCache: str = 'dump.json')
 86    def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None:
 87        """
 88        Main class init.
 89
 90        :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`.
 91        :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports.
 92                          Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.
 93        :param useCache: use default cache file with raw data to use instead of `iList`.
 94                         True by default. Cache is auto-update if new day has come.
 95                         If you don't want to use cache and always updates raw data then set `useCache=False`.
 96        :param defaultCache: path to default cache file. `dump.json` by default.
 97        """
 98        if token is None or not token:
 99            try:
100                self.token = r"{}".format(os.environ["TKS_API_TOKEN"])
101                uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/")
102
103            except KeyError:
104                uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/")
105                raise Exception("Token required")
106
107        else:
108            self.token = token  # highly priority than environment variable 'TKS_API_TOKEN'
109            uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`")
110
111        if accountId is None or not accountId:
112            try:
113                self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"])
114                uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId))
115
116            except KeyError:
117                uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).")
118
119        else:
120            self.accountId = accountId  # highly priority than environment variable 'TKS_ACCOUNT_ID'
121            uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId))
122
123        self.version = __version__  # duplicate here used TKSBrokerAPI main version
124        """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.
125
126        Latest version: https://pypi.org/project/tksbrokerapi/
127        """
128
129        self.__lock = Lock()  # initialize multiprocessing mutex lock
130
131        self.aliases = TKS_TICKER_ALIASES
132        """Some aliases instead official tickers.
133
134        See also: `TKSEnums.TKS_TICKER_ALIASES`
135        """
136
137        self.aliasesKeys = self.aliases.keys()  # re-calc only first time at class init
138
139        self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED  # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there
140
141        self._ticker = ""
142        """String with ticker, e.g. `GOOGL`. Tickers may be upper case only.
143
144        Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc.
145        More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
146
147        See also: `SearchByTicker()`, `SearchInstruments()`.
148        """
149
150        self._figi = ""
151        """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only.
152
153        See also: `SearchByFIGI()`, `SearchInstruments()`.
154        """
155
156        self.depth = 1
157        """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI.
158
159        See also: `GetCurrentPrices()`.
160        """
161
162        self.server = r"https://invest-public-api.tinkoff.ru/rest"
163        """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest
164
165        See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`.
166        """
167
168        uLogger.debug("Broker API server: {}".format(self.server))
169
170        self.timeout = 15
171        """Server operations timeout in seconds. Default: `15`.
172
173        See also: `SendAPIRequest()`.
174        """
175
176        self.headers = {
177            "Content-Type": "application/json",
178            "accept": "application/json",
179            "Authorization": "Bearer {}".format(self.token),
180            "x-app-name": "Tim55667757.TKSBrokerAPI",
181        }
182        """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`.
183
184        See also: `SendAPIRequest()`.
185        """
186
187        self.body = None
188        """Request body which send to broker server. Default: `None`.
189
190        See also: `SendAPIRequest()`.
191        """
192
193        self.moreDebug = False
194        """Enables more debug information in this class, such as net request and response headers in all methods. `False` by default."""
195
196        self.useHTMLReports = False
197        """
198        If `True` then TKSBrokerAPI generate also HTML reports from Markdown. `False` by default.
199        
200        See also: Mako Templates for Python (https://www.makotemplates.org/). Mako is a template library provides simple syntax and maximum performance.
201        """
202
203        self.historyFile = None
204        """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame.
205
206        See also: `History()`.
207        """
208
209        self.htmlHistoryFile = "index.html"
210        """Full path to the html file where rendered candles chart stored. Default: `index.html`.
211
212        See also: `ShowHistoryChart()`.
213        """
214
215        self.instrumentsFile = "instruments.md"
216        """Filename where full available to user instruments list will be saved. Default: `instruments.md`.
217
218        See also: `ShowInstrumentsInfo()`.
219        """
220
221        self.searchResultsFile = "search-results.md"
222        """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`.
223
224        See also: `SearchInstruments()`.
225        """
226
227        self.pricesFile = "prices.md"
228        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
229
230        See also: `GetListOfPrices()`.
231        """
232
233        self.infoFile = "info.md"
234        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
235
236        See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`.
237        """
238
239        self.bondsXLSXFile = "ext-bonds.xlsx"
240        """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 
241        bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`.
242
243        See also: `ExtendBondsData()`.
244        """
245
246        self.calendarFile = "calendar.md"
247        """Filename where bonds payment calendar will be saved. Default: `calendar.md`.
248        
249        Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`.
250
251        See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`.
252        """
253
254        self.overviewFile = "overview.md"
255        """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`.
256
257        See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`.
258        """
259
260        self.overviewDigestFile = "overview-digest.md"
261        """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`.
262
263        See also: `Overview()` with parameter `details="digest"`.
264        """
265
266        self.overviewPositionsFile = "overview-positions.md"
267        """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`.
268
269        See also: `Overview()` with parameter `details="positions"`.
270        """
271
272        self.overviewOrdersFile = "overview-orders.md"
273        """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`.
274
275        See also: `Overview()` with parameter `details="orders"`.
276        """
277
278        self.overviewAnalyticsFile = "overview-analytics.md"
279        """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`.
280
281        See also: `Overview()` with parameter `details="analytics"`.
282        """
283
284        self.overviewBondsCalendarFile = "overview-calendar.md"
285        """Filename where only the bonds calendar section will be saved. Default: `overview-calendar.md`.
286
287        See also: `Overview()` with parameter `details="calendar"`.
288        """
289
290        self.reportFile = "deals.md"
291        """Filename where history of deals and trade statistics will be saved. Default: `deals.md`.
292
293        See also: `Deals()`.
294        """
295
296        self.withdrawalLimitsFile = "limits.md"
297        """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`.
298
299        See also: `OverviewLimits()` and `RequestLimits()`.
300        """
301
302        self.userInfoFile = "user-info.md"
303        """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`.
304
305        See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`.
306        """
307
308        self.userAccountsFile = "accounts.md"
309        """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`.
310
311        See also: `OverviewAccounts()`, `RequestAccounts()`.
312        """
313
314        self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache
315        """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`.
316
317        Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`.
318
319        See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`.
320        """
321
322        self.iList = None  # init iList for raw instruments data
323        """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`.
324        
325        See also: `Listing()`, `DumpInstruments()`.
326        """
327
328        # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server:
329        if useCache:
330            if os.path.exists(self.iListDumpFile):
331                dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc())  # dump modification date and time
332                curTime = datetime.now(tzutc())
333
334                if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year):
335                    uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
336
337                    self.DumpInstruments(forceUpdate=True)  # updating self.iList and dump file
338
339                else:
340                    self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8"))  # load iList from dump
341
342                    uLogger.debug("Local cache with raw instruments data is used: [{}]. Last modified: [{}] UTC".format(
343                        os.path.abspath(self.iListDumpFile),
344                        dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT),
345                    ))
346
347            else:
348                uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...")
349                self.DumpInstruments(forceUpdate=True)  # updating self.iList and creating default dump file
350
351        else:
352            self.iList = self.Listing()  # request new raw instruments data from broker server
353            self.DumpInstruments(forceUpdate=False)  # save raw instrument's data to default dump file `iListDumpFile`
354
355        self.priceModel = PriceGenerator()  # init PriceGenerator object to work with candles data
356        """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.
357
358        See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator
359        """

Main class init.

Parameters
  • token: Bearer token for Tinkoff Invest API. It can be set from environment variable TKS_API_TOKEN.
  • accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports. Also, this variable can be set from environment variable TKS_ACCOUNT_ID.
  • useCache: use default cache file with raw data to use instead of iList. True by default. Cache is auto-update if new day has come. If you don't want to use cache and always updates raw data then set useCache=False.
  • defaultCache: path to default cache file. dump.json by default.
version

Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.

Latest version: https://pypi.org/project/tksbrokerapi/

aliases

Some aliases instead official tickers.

See also: TKSEnums.TKS_TICKER_ALIASES

depth

Depth of Market (DOM) can be >= 1. Default: 1. It used with --price key to showing DOM with current prices for givens ticker or FIGI.

See also: GetCurrentPrices().

server

Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest

See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and SendAPIRequest().

timeout

Server operations timeout in seconds. Default: 15.

See also: SendAPIRequest().

headers

Headers which send in every request to broker server. Please, do not change it! Default: {"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}.

See also: SendAPIRequest().

body

Request body which send to broker server. Default: None.

See also: SendAPIRequest().

moreDebug

Enables more debug information in this class, such as net request and response headers in all methods. False by default.

useHTMLReports

If True then TKSBrokerAPI generate also HTML reports from Markdown. False by default.

See also: Mako Templates for Python (https://www.makotemplates.org/). Mako is a template library provides simple syntax and maximum performance.

historyFile

Full path to the output file where history candles will be saved or updated. Default: None, it mean that returns only Pandas DataFrame.

See also: History().

htmlHistoryFile

Full path to the html file where rendered candles chart stored. Default: index.html.

See also: ShowHistoryChart().

instrumentsFile

Filename where full available to user instruments list will be saved. Default: instruments.md.

See also: ShowInstrumentsInfo().

searchResultsFile

Filename with all found instruments searched by part of its ticker, FIGI or name. Default: search-results.md.

See also: SearchInstruments().

pricesFile

Filename where prices of selected instruments will be saved. Default: prices.md.

See also: GetListOfPrices().

infoFile

Filename where prices of selected instruments will be saved. Default: prices.md.

See also: ShowInstrumentsInfo(), RequestBondCoupons() and RequestTradingStatus().

bondsXLSXFile

Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, bonds payment calendar, some statistics will be stored. Default: ext-bonds.xlsx.

See also: ExtendBondsData().

calendarFile

Filename where bonds payment calendar will be saved. Default: calendar.md.

Pandas dataframe with only bonds payment calendar also will be stored to default file calendar.xlsx.

See also: CreateBondsCalendar(), ShowBondsCalendar(), ShowInstrumentInfo(), RequestBondCoupons() and ExtendBondsData().

overviewFile

Filename where current portfolio, open trades and orders will be saved. Default: overview.md.

See also: Overview(), RequestPortfolio(), RequestPositions(), RequestPendingOrders() and RequestStopOrders().

overviewDigestFile

Filename where short digest of the portfolio status will be saved. Default: overview-digest.md.

See also: Overview() with parameter details="digest".

overviewPositionsFile

Filename where only open positions, without everything else will be saved. Default: overview-positions.md.

See also: Overview() with parameter details="positions".

overviewOrdersFile

Filename where open limits and stop orders will be saved. Default: overview-orders.md.

See also: Overview() with parameter details="orders".

overviewAnalyticsFile

Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: overview-analytics.md.

See also: Overview() with parameter details="analytics".

overviewBondsCalendarFile

Filename where only the bonds calendar section will be saved. Default: overview-calendar.md.

See also: Overview() with parameter details="calendar".

reportFile

Filename where history of deals and trade statistics will be saved. Default: deals.md.

See also: Deals().

withdrawalLimitsFile

Filename where table of funds available for withdrawal will be saved. Default: limits.md.

See also: OverviewLimits() and RequestLimits().

userInfoFile

Filename where all available user's data (accountIds, common user information, margin status and tariff connections limit) will be saved. Default: user-info.md.

See also: OverviewUserInfo(), RequestAccounts(), RequestUserInfo(), RequestMarginStatus() and RequestTariffLimits().

userAccountsFile

Filename where simple table with all available user accounts (accountIds) will be saved. Default: accounts.md.

See also: OverviewAccounts(), RequestAccounts().

iListDumpFile

Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: dump.json.

Pandas dataframe with raw instruments data also will be stored to default file dump.xlsx.

See also: DumpInstruments() and DumpInstrumentsAsXLSX().

iList

Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the iListDumpFile.

See also: Listing(), DumpInstruments().

priceModel

PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.

See also: LoadHistory(), ShowHistoryChart() and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator

ticker: str

Setter for string with ticker, e.g. GOOGL. Tickers may be upper case only.

Use alias for USD000UTSTOM simple as USD, EUR_RUB__TOM as EUR etc. More tickers aliases here: TKSEnums.TKS_TICKER_ALIASES.

See also: SearchByTicker(), SearchInstruments().

figi: str

Setter for string with FIGI, e.g. ticker GOOGL has FIGI BBG009S39JX6. FIGIs may be upper case only.

See also: SearchByFIGI(), SearchInstruments().

def SendAPIRequest( self, url: str, reqType: str = 'GET', retry: int = 3, pause: int = 5) -> dict:
418    def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5) -> dict:
419        """
420        Send GET or POST request to broker server and receive JSON object.
421
422        self.header: must be defining with dictionary of headers.
423        self.body: if define then used as request body. None by default.
424        self.timeout: global request timeout, 15 seconds by default.
425        :param url: url with REST request.
426        :param reqType: send "GET" or "POST" request. "GET" by default.
427        :param retry: how many times retry after first request if an 5xx server errors occurred.
428        :param pause: sleep time in seconds between retries.
429        :return: response JSON (dictionary) from broker.
430        """
431        if reqType.upper() not in ("GET", "POST"):
432            uLogger.error("You can define request type: `GET` or `POST`!")
433            raise Exception("Incorrect value")
434
435        if self.moreDebug:
436            uLogger.debug("Request parameters:")
437            uLogger.debug("    - REST API URL: {}".format(url))
438            uLogger.debug("    - request type: {}".format(reqType))
439            uLogger.debug("    - headers:\n{}".format(str(self.headers).replace(self.token, "*** request token ***")))
440            uLogger.debug("    - body:\n{}".format(self.body))
441
442        # fast hack to avoid all operations with some tickers/FIGI
443        responseJSON = {}
444        oK = True
445        for item in self.exclude:
446            if item in url:
447                if self.moreDebug:
448                    uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude)))
449
450                oK = False
451                break
452
453        if oK:
454            with self.__lock:  # acquire the mutex lock
455                counter = 0
456                response = None
457                errMsg = ""
458
459                while not response and counter <= retry:
460                    if reqType == "GET":
461                        response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout)
462
463                    if reqType == "POST":
464                        response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout)
465
466                    if self.moreDebug:
467                        uLogger.debug("Response:")
468                        uLogger.debug("    - status code: {}".format(response.status_code))
469                        uLogger.debug("    - reason: {}".format(response.reason))
470                        uLogger.debug("    - body length: {}".format(len(response.text)))
471                        uLogger.debug("    - headers:\n{}".format(response.headers))
472
473                    # Server returns some headers:
474                    # - `x-ratelimit-limit` — shows the settings of the current user limit for this method.
475                    # - `x-ratelimit-remaining` — the number of remaining requests of this type per minute.
476                    # - `x-ratelimit-reset` — time in seconds before resetting the request counter.
477                    # See: https://tinkoff.github.io/investAPI/grpc/#kreya
478                    if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0":
479                        rateLimitWait = int(response.headers["x-ratelimit-reset"])
480                        uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait))
481                        sleep(rateLimitWait)
482
483                    # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes
484                    if 400 <= response.status_code < 500:
485                        msg = "status code: [{}], response body: {}".format(response.status_code, response.text)
486                        uLogger.debug("    - not oK, but do not retry for 4xx errors, {}".format(msg))
487
488                        if "code" in response.text and "message" in response.text:
489                            msgDict = self._ParseJSON(rawData=response.text)
490                            uLogger.warning("HTTP-status code [{}], server message: {}".format(response.status_code, msgDict["message"]))
491
492                        counter = retry + 1  # do not retry for 4xx errors
493
494                    if 500 <= response.status_code < 600:
495                        errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text)
496                        uLogger.debug("    - not oK, {}".format(errMsg))
497
498                        if "code" in response.text and "message" in response.text:
499                            errMsgDict = self._ParseJSON(rawData=response.text)
500                            uLogger.warning("HTTP-status code [{}], error message: {}".format(response.status_code, errMsgDict["message"]))
501
502                        counter += 1
503
504                        if counter <= retry:
505                            uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause))
506                            sleep(pause)
507
508                responseJSON = self._ParseJSON(rawData=response.text)
509
510                if errMsg:
511                    uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/")
512                    uLogger.error("    - not oK, {}".format(errMsg))
513
514        return responseJSON

Send GET or POST request to broker server and receive JSON object.

self.header: must be defining with dictionary of headers. self.body: if define then used as request body. None by default. self.timeout: global request timeout, 15 seconds by default.

Parameters
  • url: url with REST request.
  • reqType: send "GET" or "POST" request. "GET" by default.
  • retry: how many times retry after first request if an 5xx server errors occurred.
  • pause: sleep time in seconds between retries.
Returns

response JSON (dictionary) from broker.

def Listing(self) -> dict:
547    def Listing(self) -> dict:
548        """
549        Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.
550
551        :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.
552        """
553        uLogger.debug("Requesting all available instruments for current account. Wait, please...")
554        uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES))
555
556        # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService
557        # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list.
558        iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS]
559
560        poolUpdater = ThreadPool(processes=CPU_USAGES)  # create pool for update instruments in parallel mode
561        listing = poolUpdater.map(self._IWrapper, iParams)  # execute update operations
562        poolUpdater.close()  # close the thread pool
563        poolUpdater.join()  # wait a moment until all data returns from threads
564
565        # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures.
566        # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method
567        iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing}
568
569        # calculate minimum price increment (step) for all instruments and set up instrument's type:
570        for iType in iList.keys():
571            for ticker in iList[iType]:
572                iList[iType][ticker]["type"] = iType
573
574                if "minPriceIncrement" in iList[iType][ticker].keys():
575                    iList[iType][ticker]["step"] = NanoToFloat(
576                        iList[iType][ticker]["minPriceIncrement"]["units"],
577                        iList[iType][ticker]["minPriceIncrement"]["nano"],
578                    )
579
580                else:
581                    iList[iType][ticker]["step"] = 0  # hack to avoid empty value in some instruments, e.g. futures
582
583        return iList

Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.

Returns

Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.

def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None:
585    def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None:
586        """
587        Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.
588
589        See also: `DumpInstruments()`, `Listing()`.
590
591        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
592                            otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) .
593        """
594        if self.iListDumpFile is None or not self.iListDumpFile:
595            uLogger.error("Output name of dump file must be defined!")
596            raise Exception("Filename required")
597
598        if not self.iList or forceUpdate:
599            self.iList = self.Listing()
600
601        xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx"
602
603        # Save as XLSX with separated sheets for every type of instruments:
604        with pd.ExcelWriter(
605                path=xlsxDumpFile,
606                date_format=TKS_DATE_FORMAT,
607                datetime_format=TKS_DATE_TIME_FORMAT,
608                mode="w",
609        ) as writer:
610            for iType in TKS_INSTRUMENTS:
611                df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index")  # generate pandas object from self.iList dictionary
612                df = df[sorted(df)]  # sorted by column names
613                df = df.applymap(
614                    lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item,
615                    na_action="ignore",
616                )  # converting numbers from nano-type to float in every cell
617                df.to_excel(
618                    writer,
619                    sheet_name=iType,
620                    encoding="UTF-8",
621                    freeze_panes=(1, 1),
622                )  # saving as XLSX-file with freeze first row and column as headers
623
624        uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile)))

Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.

See also: DumpInstruments(), Listing().

Parameters
  • forceUpdate: if True then at first updates data with Listing() method, otherwise just saves exist iList as XLSX-file (default: dump.xlsx) .
def DumpInstruments(self, forceUpdate: bool = True) -> str:
626    def DumpInstruments(self, forceUpdate: bool = True) -> str:
627        """
628        Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server
629        using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file.
630
631        See also: `DumpInstrumentsAsXLSX()`, `Listing()`.
632
633        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
634                            otherwise just saves exist `iList` as JSON-file (default: `dump.json`).
635        :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file.
636        """
637        if self.iListDumpFile is None or not self.iListDumpFile:
638            uLogger.error("Output name of dump file must be defined!")
639            raise Exception("Filename required")
640
641        if not self.iList or forceUpdate:
642            self.iList = self.Listing()
643
644        jsonDump = json.dumps(self.iList, indent=4, sort_keys=False)  # create JSON object as string
645        with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH:
646            fH.write(jsonDump)
647
648        uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile)))
649
650        return jsonDump

Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server using Listing() method. If iListDumpFile string is not empty then also save information to this file.

See also: DumpInstrumentsAsXLSX(), Listing().

Parameters
  • forceUpdate: if True then at first updates data with Listing() method, otherwise just saves exist iList as JSON-file (default: dump.json).
Returns

serialized JSON formatted str with full data of instruments, also saved to the --output JSON-file.

def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str:
652    def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str:
653        """
654        Show information about one instrument defined by json data and prints it in Markdown format.
655
656        See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`.
657
658        :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self._ticker]`
659        :param show: if `True` then also printing information about instrument and its current price.
660        :return: multilines text in Markdown format with information about one instrument.
661        """
662        splitLine = "|                                                             |                                                        |\n"
663        infoText = ""
664
665        if iJSON is not None and iJSON and isinstance(iJSON, dict):
666            info = [
667                "# Main information\n\n",
668                "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
669                "| Parameters                                                  | Values                                                 |\n",
670                "|-------------------------------------------------------------|--------------------------------------------------------|\n",
671                "| Ticker:                                                     | {:<54} |\n".format(iJSON["ticker"]),
672                "| Full name:                                                  | {:<54} |\n".format(iJSON["name"]),
673            ]
674
675            if "sector" in iJSON.keys() and iJSON["sector"]:
676                info.append("| Sector:                                                     | {:<54} |\n".format(iJSON["sector"]))
677
678            if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] and "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"]:
679                info.append("| Country of instrument:                                      | {:<54} |\n".format("({}) {}".format(iJSON["countryOfRisk"], iJSON["countryOfRiskName"])))
680
681            info.extend([
682                splitLine,
683                "| FIGI (Financial Instrument Global Identifier):              | {:<54} |\n".format(iJSON["figi"]),
684                "| Real exchange [Exchange section]:                           | {:<54} |\n".format("{} [{}]".format(TKS_REAL_EXCHANGES[iJSON["realExchange"]], iJSON["exchange"])),
685            ])
686
687            if "isin" in iJSON.keys() and iJSON["isin"]:
688                info.append("| ISIN (International Securities Identification Number):      | {:<54} |\n".format(iJSON["isin"]))
689
690            if "classCode" in iJSON.keys():
691                info.append("| Class Code (exchange section where instrument is traded):   | {:<54} |\n".format(iJSON["classCode"]))
692
693            info.extend([
694                splitLine,
695                "| Current broker security trading status:                     | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]),
696                splitLine,
697                "| Buy operations allowed:                                     | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"),
698                "| Sale operations allowed:                                    | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"),
699                "| Short positions allowed:                                    | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"),
700            ])
701
702            if iJSON["figi"]:
703                self._figi = iJSON["figi"]
704                iJSON = iJSON | self.RequestTradingStatus()
705
706                info.extend([
707                    splitLine,
708                    "| Limit orders allowed:                                       | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"),
709                    "| Market orders allowed:                                      | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"),
710                    "| API trade allowed:                                          | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"),
711                ])
712
713            info.append(splitLine)
714
715            if "type" in iJSON.keys() and iJSON["type"]:
716                info.append("| Type of the instrument:                                     | {:<54} |\n".format(iJSON["type"]))
717
718                if "shareType" in iJSON.keys() and iJSON["shareType"]:
719                    info.append("| Share type:                                                 | {:<54} |\n".format(TKS_SHARE_TYPES[iJSON["shareType"]]))
720
721            if "futuresType" in iJSON.keys() and iJSON["futuresType"]:
722                info.append("| Futures type:                                               | {:<54} |\n".format(iJSON["futuresType"]))
723
724            if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]:
725                info.append("| IPO date:                                                   | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", "")))
726
727            if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]:
728                info.append("| Released date:                                              | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", "")))
729
730            if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]:
731                info.append("| Rebalancing frequency:                                      | {:<54} |\n".format(iJSON["rebalancingFreq"]))
732
733            if "focusType" in iJSON.keys() and iJSON["focusType"]:
734                info.append("| Focusing type:                                              | {:<54} |\n".format(iJSON["focusType"]))
735
736            if "assetType" in iJSON.keys() and iJSON["assetType"]:
737                info.append("| Asset type:                                                 | {:<54} |\n".format(iJSON["assetType"]))
738
739            if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]:
740                info.append("| Basic asset:                                                | {:<54} |\n".format(iJSON["basicAsset"]))
741
742            if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]:
743                info.append("| Basic asset size:                                           | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"]))))
744
745            if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]:
746                info.append("| ISO currency name:                                          | {:<54} |\n".format(iJSON["isoCurrencyName"]))
747
748            if "currency" in iJSON.keys():
749                info.append("| Payment currency:                                           | {:<54} |\n".format(iJSON["currency"]))
750
751            if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys():
752                info.append("| Nominal currency:                                           | {:<54} |\n".format(iJSON["nominal"]["currency"]))
753
754            if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]:
755                info.append("| First trade date:                                           | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", "")))
756
757            if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]:
758                info.append("| Last trade date:                                            | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", "")))
759
760            if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]:
761                info.append("| Date of expiration:                                         | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", "")))
762
763            if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]:
764                info.append("| State registration date:                                    | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", "")))
765
766            if "placementDate" in iJSON.keys() and iJSON["placementDate"]:
767                info.append("| Placement date:                                             | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", "")))
768
769            if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]:
770                info.append("| Maturity date:                                              | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", "")))
771
772            if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]:
773                info.append("| Perpetual bond:                                             | Yes                                                    |\n")
774
775            if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]:
776                info.append("| Over-the-counter (OTC) securities:                          | Yes                                                    |\n")
777
778            iExt = None
779            if iJSON["type"] == "Bonds":
780                info.extend([
781                    splitLine,
782                    "| Bond issue (size / plan):                                   | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])),
783                    "| Nominal price (100%):                                       | {:<54} |\n".format("{} {}".format(
784                        "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."),
785                        iJSON["nominal"]["currency"],
786                    )),
787                ])
788
789                if "floatingCouponFlag" in iJSON.keys():
790                    info.append("| Floating coupon:                                            | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No"))
791
792                if "amortizationFlag" in iJSON.keys():
793                    info.append("| Amortization:                                               | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No"))
794
795                info.append(splitLine)
796
797                if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]:
798                    info.append("| Number of coupon payments per year:                         | {:<54} |\n".format(iJSON["couponQuantityPerYear"]))
799
800                if iJSON["figi"]:
801                    iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False)  # extended bonds data
802
803                    info.extend([
804                        "| Days last to maturity date:                                 | {:<54} |\n".format(iExt["daysToMaturity"][0]),
805                        "| Coupons yield (average coupon daily yield * 365):           | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])),
806                        "| Current price yield (average daily yield * 365):            | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])),
807                    ])
808
809                if "aciValue" in iJSON.keys() and iJSON["aciValue"]:
810                    info.append("| Current accumulated coupon income (ACI):                    | {:<54} |\n".format("{:.2f} {}".format(
811                        NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]),
812                        iJSON["aciValue"]["currency"]
813                    )))
814
815            if "currentPrice" in iJSON.keys():
816                info.append(splitLine)
817
818                currency = iJSON["currency"] if "currency" in iJSON.keys() else ""  # nominal currency for bonds, otherwise currency of instrument
819                aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else ""  # payment currency
820
821                bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0  # previous close price of bond
822                bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0  # last price of bond
823                bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0  # max price of bond
824                bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0  # min price of bond
825                bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0  # delta between last deal price and last close
826
827                curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0
828                curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0
829
830                info.extend([
831                    "| Previous close price of the instrument:                     | {:<54} |\n".format("{}{}".format(
832                        "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A",
833                        "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
834                    )),
835                    "| Last deal price of the instrument:                          | {:<54} |\n".format("{}{}".format(
836                        "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A",
837                        "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
838                    )),
839                    "| Changes between last deal price and last close              | {:<54} |\n".format(
840                        "{:.2f}%{}".format(
841                            iJSON["currentPrice"]["changes"],
842                            " ({}{:.2f} {})".format(
843                                "+" if bondChangesDelta > 0 else "",
844                                bondChangesDelta,
845                                aciCurrency
846                            ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format(
847                                "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "",
848                                iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"],
849                                currency
850                            ),
851                        )
852                    ),
853                    "| Current limit price, min / max:                             | {:<54} |\n".format("{}{} / {}{}{}".format(
854                        "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A",
855                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
856                        "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A",
857                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
858                        " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else ""
859                    )),
860                    "| Actual price, sell / buy:                                   | {:<54} |\n".format("{}{} / {}{}{}".format(
861                        "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A",
862                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
863                        "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A",
864                        "%" if iJSON["type"] == "Bonds" else" {}".format(currency),
865                        " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else ""
866                    )),
867                ])
868
869            if "lot" in iJSON.keys():
870                info.append("| Minimum lot to buy:                                         | {:<54} |\n".format(iJSON["lot"]))
871
872            if "step" in iJSON.keys() and iJSON["step"] != 0:
873                info.append("| Minimum price increment (step):                             | {:<54} |\n".format("{} {}".format(iJSON["step"], iJSON["currency"] if "currency" in iJSON.keys() else "")))
874
875            # Add bond payment calendar:
876            if iJSON["type"] == "Bonds":
877                strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False)   # bond payment calendar
878                info.extend(["\n#", strCalendar])
879
880            infoText += "".join(info)
881
882            if show:
883                uLogger.info("{}".format(infoText))
884
885            else:
886                uLogger.debug("{}".format(infoText))
887
888            if self.infoFile is not None:
889                with open(self.infoFile, "w", encoding="UTF-8") as fH:
890                    fH.write(infoText)
891
892                uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile)))
893
894                if self.useHTMLReports:
895                    htmlFilePath = self.infoFile.replace(".md", ".html") if self.infoFile.endswith(".md") else self.infoFile + ".html"
896                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
897                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Main information", commonCSS=COMMON_CSS, markdown=infoText))
898
899                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
900
901        return infoText

Show information about one instrument defined by json data and prints it in Markdown format.

See also: SearchByTicker(), SearchByFIGI(), RequestBondCoupons(), ExtendBondsData(), ShowBondsCalendar() and RequestTradingStatus().

Parameters
  • iJSON: json data of instrument, example: iJSON = self.iList["Shares"][self._ticker]
  • show: if True then also printing information about instrument and its current price.
Returns

multilines text in Markdown format with information about one instrument.

def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict:
903    def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict:
904        """
905        Search and return raw broker's information about instrument by its ticker. Variable `ticker` must be defined!
906
907        :param requestPrice: if `False` then do not request current price of instrument (because this is long operation).
908        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
909        :return: JSON formatted data with information about instrument.
910        """
911        tickerJSON = {}
912        if self.moreDebug:
913            uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self._ticker))
914
915        if not self._ticker:
916            uLogger.warning("self._ticker variable is not be empty!")
917
918        else:
919            if self._ticker in TKS_TICKERS_OR_FIGI_EXCLUDED:
920                uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self._ticker))
921                raise Exception("Instrument not allowed")
922
923            if not self.iList:
924                self.iList = self.Listing()
925
926            if self._ticker in self.iList["Shares"].keys():
927                tickerJSON = self.iList["Shares"][self._ticker]
928                if self.moreDebug:
929                    uLogger.debug("Ticker [{}] found in shares list".format(self._ticker))
930
931            elif self._ticker in self.iList["Currencies"].keys():
932                tickerJSON = self.iList["Currencies"][self._ticker]
933                if self.moreDebug:
934                    uLogger.debug("Ticker [{}] found in currencies list".format(self._ticker))
935
936            elif self._ticker in self.iList["Bonds"].keys():
937                tickerJSON = self.iList["Bonds"][self._ticker]
938                if self.moreDebug:
939                    uLogger.debug("Ticker [{}] found in bonds list".format(self._ticker))
940
941            elif self._ticker in self.iList["Etfs"].keys():
942                tickerJSON = self.iList["Etfs"][self._ticker]
943                if self.moreDebug:
944                    uLogger.debug("Ticker [{}] found in etfs list".format(self._ticker))
945
946            elif self._ticker in self.iList["Futures"].keys():
947                tickerJSON = self.iList["Futures"][self._ticker]
948                if self.moreDebug:
949                    uLogger.debug("Ticker [{}] found in futures list".format(self._ticker))
950
951        if tickerJSON:
952            self._figi = tickerJSON["figi"]
953
954            if requestPrice:
955                tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False)
956
957                if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None:
958                    tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"]
959
960                else:
961                    tickerJSON["currentPrice"]["changes"] = 0
962
963            if show:
964                self.ShowInstrumentInfo(iJSON=tickerJSON, show=True)  # print info as Markdown text
965
966        else:
967            if show:
968                uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self._ticker))
969
970        return tickerJSON

Search and return raw broker's information about instrument by its ticker. Variable ticker must be defined!

Parameters
  • requestPrice: if False then do not request current price of instrument (because this is long operation).
  • show: if False then do not run ShowInstrumentInfo() method and do not print info to the console.
Returns

JSON formatted data with information about instrument.

def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict:
 972    def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict:
 973        """
 974        Search and return raw broker's information about instrument by its FIGI. Variable `figi` must be defined!
 975
 976        :param requestPrice: if `False` then do not request current price of instrument (it's long operation).
 977        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
 978        :return: JSON formatted data with information about instrument.
 979        """
 980        figiJSON = {}
 981        if self.moreDebug:
 982            uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self._figi))
 983
 984        if not self._figi:
 985            uLogger.warning("self._figi variable is not be empty!")
 986
 987        else:
 988            if self._figi in TKS_TICKERS_OR_FIGI_EXCLUDED:
 989                uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self._figi))
 990                raise Exception("Instrument not allowed")
 991
 992            if not self.iList:
 993                self.iList = self.Listing()
 994
 995            for item in self.iList["Shares"].keys():
 996                if self._figi == self.iList["Shares"][item]["figi"]:
 997                    figiJSON = self.iList["Shares"][item]
 998
 999                    if self.moreDebug:
1000                        uLogger.debug("FIGI [{}] found in shares list".format(self._figi))
1001
1002                    break
1003
1004            if not figiJSON:
1005                for item in self.iList["Currencies"].keys():
1006                    if self._figi == self.iList["Currencies"][item]["figi"]:
1007                        figiJSON = self.iList["Currencies"][item]
1008
1009                        if self.moreDebug:
1010                            uLogger.debug("FIGI [{}] found in currencies list".format(self._figi))
1011
1012                        break
1013
1014            if not figiJSON:
1015                for item in self.iList["Bonds"].keys():
1016                    if self._figi == self.iList["Bonds"][item]["figi"]:
1017                        figiJSON = self.iList["Bonds"][item]
1018
1019                        if self.moreDebug:
1020                            uLogger.debug("FIGI [{}] found in bonds list".format(self._figi))
1021
1022                        break
1023
1024            if not figiJSON:
1025                for item in self.iList["Etfs"].keys():
1026                    if self._figi == self.iList["Etfs"][item]["figi"]:
1027                        figiJSON = self.iList["Etfs"][item]
1028
1029                        if self.moreDebug:
1030                            uLogger.debug("FIGI [{}] found in etfs list".format(self._figi))
1031
1032                        break
1033
1034            if not figiJSON:
1035                for item in self.iList["Futures"].keys():
1036                    if self._figi == self.iList["Futures"][item]["figi"]:
1037                        figiJSON = self.iList["Futures"][item]
1038
1039                        if self.moreDebug:
1040                            uLogger.debug("FIGI [{}] found in futures list".format(self._figi))
1041
1042                        break
1043
1044        if figiJSON:
1045            self._figi = figiJSON["figi"]
1046            self._ticker = figiJSON["ticker"]
1047
1048            if requestPrice:
1049                figiJSON["currentPrice"] = self.GetCurrentPrices(show=False)
1050
1051                if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None:
1052                    figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"]
1053
1054                else:
1055                    figiJSON["currentPrice"]["changes"] = 0
1056
1057            if show:
1058                self.ShowInstrumentInfo(iJSON=figiJSON, show=True)  # print info as Markdown text
1059
1060        else:
1061            if show:
1062                uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self._figi))
1063
1064        return figiJSON

Search and return raw broker's information about instrument by its FIGI. Variable figi must be defined!

Parameters
  • requestPrice: if False then do not request current price of instrument (it's long operation).
  • show: if False then do not run ShowInstrumentInfo() method and do not print info to the console.
Returns

JSON formatted data with information about instrument.

def GetCurrentPrices(self, show: bool = True) -> dict:
1066    def GetCurrentPrices(self, show: bool = True) -> dict:
1067        """
1068        Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5:
1069        `{"buy": [{"price": 1243.8, "quantity": 193},
1070                  {"price": 1244.0, "quantity": 168},
1071                  {"price": 1244.8, "quantity": 5},
1072                  {"price": 1245.0, "quantity": 61},
1073                  {"price": 1245.4, "quantity": 60}],
1074          "sell": [{"price": 1243.6, "quantity": 8},
1075                   {"price": 1242.6, "quantity": 10},
1076                   {"price": 1242.4, "quantity": 18},
1077                   {"price": 1242.2, "quantity": 50},
1078                   {"price": 1242.0, "quantity": 113}],
1079          "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean:
1080        - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order
1081        - sell: list of dicts with Buyers prices,
1082            - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument),
1083            - quantity: volume value by current price in lots,
1084        - limitUp: current trade session limit price, maximum,
1085        - limitDown: current trade session limit price, minimum,
1086        - lastPrice: last deal price of the instrument,
1087        - closePrice: previous trade session close price of the instrument.
1088
1089        See also: `SearchByTicker()` and `SearchByFIGI()`.
1090        REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1091        Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1092
1093        :param show: if `True` then print DOM to log and console.
1094        :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`.
1095                 If an error occurred then returns an empty record:
1096                 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`.
1097        """
1098        prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0}
1099
1100        if self.depth < 1:
1101            uLogger.error("Depth of Market (DOM) must be >=1!")
1102            raise Exception("Incorrect value")
1103
1104        if not (self._ticker or self._figi):
1105            uLogger.error("self._ticker or self._figi variables must be defined!")
1106            raise Exception("Ticker or FIGI required")
1107
1108        if self._ticker and not self._figi:
1109            instrumentByTicker = self.SearchByTicker(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1110            self._figi = instrumentByTicker["figi"] if instrumentByTicker else ""
1111
1112        if not self._ticker and self._figi:
1113            instrumentByFigi = self.SearchByFIGI(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1114            self._ticker = instrumentByFigi["ticker"] if instrumentByFigi else ""
1115
1116        if not self._figi:
1117            uLogger.error("FIGI is not defined!")
1118            raise Exception("Ticker or FIGI required")
1119
1120        else:
1121            uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self._ticker, self._figi))
1122
1123            # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1124            priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook"
1125            self.body = str({"figi": self._figi, "depth": self.depth})
1126            pricesResponse = self.SendAPIRequest(priceURL, reqType="POST")  # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1127
1128            if pricesResponse and not ("code" in pricesResponse.keys() or "message" in pricesResponse.keys() or "description" in pricesResponse.keys()):
1129                # list of dicts with sellers orders:
1130                prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]]
1131
1132                # list of dicts with buyers orders:
1133                prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]]
1134
1135                # max price of instrument at this time:
1136                prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None
1137
1138                # min price of instrument at this time:
1139                prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None
1140
1141                # last price of deal with instrument:
1142                prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0
1143
1144                # last close price of instrument:
1145                prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0
1146
1147            else:
1148                uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi))
1149                uLogger.debug("Server response: {}".format(pricesResponse))
1150
1151            if show:
1152                if prices["buy"] or prices["sell"]:
1153                    info = [
1154                        "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format(
1155                            datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
1156                            self._ticker,
1157                            self._figi,
1158                            self.depth,
1159                        ),
1160                        "-" * 60, "\n",
1161                        "             Orders of Buyers | Orders of Sellers\n",
1162                        "-" * 60, "\n",
1163                        "        Sell prices (volumes) | Buy prices (volumes)\n",
1164                        "-" * 60, "\n",
1165                    ]
1166
1167                    if not prices["buy"]:
1168                        info.append("                              | No orders!\n")
1169                        sumBuy = 0
1170
1171                    else:
1172                        sumBuy = sum([x["quantity"] for x in prices["buy"]])
1173                        maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True)
1174                        for item in maxMinSorted:
1175                            info.append("                              | {} ({})\n".format(item["price"], item["quantity"]))
1176
1177                    if not prices["sell"]:
1178                        info.append("No orders!                    |\n")
1179                        sumSell = 0
1180
1181                    else:
1182                        sumSell = sum([x["quantity"] for x in prices["sell"]])
1183                        for item in prices["sell"]:
1184                            info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"])))
1185
1186                    info.extend([
1187                        "-" * 60, "\n",
1188                        "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)),
1189                        "-" * 60, "\n",
1190                    ])
1191
1192                    infoText = "".join(info)
1193
1194                    uLogger.info("Current prices in order book:\n\n{}".format(infoText))
1195
1196                else:
1197                    uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi))
1198
1199        return prices

Get and show Depth of Market with current prices of the instrument as dictionary. Result example with depth 5: {"buy": [{"price": 1243.8, "quantity": 193}, {"price": 1244.0, "quantity": 168}, {"price": 1244.8, "quantity": 5}, {"price": 1245.0, "quantity": 61}, {"price": 1245.4, "quantity": 60}], "sell": [{"price": 1243.6, "quantity": 8}, {"price": 1242.6, "quantity": 10}, {"price": 1242.4, "quantity": 18}, {"price": 1242.2, "quantity": 50}, {"price": 1242.0, "quantity": 113}], "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}, where parameters mean:

  • buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order
  • sell: list of dicts with Buyers prices,
    • price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument),
    • quantity: volume value by current price in lots,
  • limitUp: current trade session limit price, maximum,
  • limitDown: current trade session limit price, minimum,
  • lastPrice: last deal price of the instrument,
  • closePrice: previous trade session close price of the instrument.

See also: SearchByTicker() and SearchByFIGI(). REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse

Parameters
  • show: if True then print DOM to log and console.
Returns

orders book dict with lists of current buy and sell prices: {"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}. If an error occurred then returns an empty record: {"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}.

def ShowInstrumentsInfo(self, show: bool = True) -> str:
1201    def ShowInstrumentsInfo(self, show: bool = True) -> str:
1202        """
1203        This method get and show information about all available broker instruments for current user account.
1204        If `instrumentsFile` string is not empty then also save information to this file.
1205
1206        :param show: if `True` then print results to console, if `False` — print only to file.
1207        :return: multi-lines string with all available broker instruments
1208        """
1209        if not self.iList:
1210            self.iList = self.Listing()
1211
1212        info = [
1213            "# All available instruments from Tinkoff Broker server for current user token\n\n",
1214            "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
1215        ]
1216
1217        # add instruments count by type:
1218        for iType in self.iList.keys():
1219            info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType])))
1220
1221        headerLine = "| Ticker       | Full name                                                 | FIGI         | Cur | Lot     | Step       |\n"
1222        splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n"
1223
1224        # generating info tables with all instruments by type:
1225        for iType in self.iList.keys():
1226            info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine])
1227
1228            for instrument in self.iList[iType].keys():
1229                iName = self.iList[iType][instrument]["name"]  # instrument's name
1230                if len(iName) > 57:
1231                    iName = "{}...".format(iName[:54])  # right trim for a long string
1232
1233                info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format(
1234                    self.iList[iType][instrument]["ticker"],
1235                    iName,
1236                    self.iList[iType][instrument]["figi"],
1237                    self.iList[iType][instrument]["currency"],
1238                    self.iList[iType][instrument]["lot"],
1239                    "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0,
1240                ))
1241
1242        infoText = "".join(info)
1243
1244        if show:
1245            uLogger.info(infoText)
1246
1247        if self.instrumentsFile:
1248            with open(self.instrumentsFile, "w", encoding="UTF-8") as fH:
1249                fH.write(infoText)
1250
1251            uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile)))
1252
1253            if self.useHTMLReports:
1254                htmlFilePath = self.instrumentsFile.replace(".md", ".html") if self.instrumentsFile.endswith(".md") else self.instrumentsFile + ".html"
1255                with open(htmlFilePath, "w", encoding="UTF-8") as fH:
1256                    fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="List of instruments", commonCSS=COMMON_CSS, markdown=infoText))
1257
1258                uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
1259
1260        return infoText

This method get and show information about all available broker instruments for current user account. If instrumentsFile string is not empty then also save information to this file.

Parameters
  • show: if True then print results to console, if False — print only to file.
Returns

multi-lines string with all available broker instruments

def SearchInstruments(self, pattern: str, show: bool = True) -> dict:
1262    def SearchInstruments(self, pattern: str, show: bool = True) -> dict:
1263        """
1264        This method search and show information about instruments by part of its ticker, FIGI or name.
1265        If `searchResultsFile` string is not empty then also save information to this file.
1266
1267        :param pattern: string with part of ticker, FIGI or instrument's name.
1268        :param show: if `True` then print results to console, if `False` — return list of result only.
1269        :return: list of dictionaries with all found instruments.
1270        """
1271        if not self.iList:
1272            self.iList = self.Listing()
1273
1274        searchResults = {iType: {} for iType in self.iList}  # same as iList but will contain only filtered instruments
1275        compiledPattern = re.compile(pattern, re.IGNORECASE)
1276
1277        for iType in self.iList:
1278            for instrument in self.iList[iType].values():
1279                searchResult = compiledPattern.search(" ".join(
1280                    [instrument["ticker"], instrument["figi"], instrument["name"]]
1281                ))
1282
1283                if searchResult:
1284                    searchResults[iType][instrument["ticker"]] = instrument
1285
1286        resultsLen = sum([len(searchResults[iType]) for iType in searchResults])
1287        info = [
1288            "# Search results\n\n",
1289            "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
1290            "* **Search pattern:** [{}]\n".format(pattern),
1291            "* **Found instruments:** [{}]\n\n".format(resultsLen),
1292            '**Note:** you can view info about found instruments with key "--info", e.g.: "tksbrokerapi -t TICKER --info" or "tksbrokerapi -f FIGI --info".\n'
1293        ]
1294        infoShort = info[:]
1295
1296        headerLine = "| Type       | Ticker       | Full name                                                      | FIGI         |\n"
1297        splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n"
1298        skippedLine = "| ...        | ...          | ...                                                            | ...          |\n"
1299
1300        if resultsLen == 0:
1301            info.append("\nNo results\n")
1302            infoShort.append("\nNo results\n")
1303            uLogger.warning("No results. Try changing your search pattern.")
1304
1305        else:
1306            for iType in searchResults:
1307                iTypeValuesCount = len(searchResults[iType].values())
1308                if iTypeValuesCount > 0:
1309                    info.extend(["\n## {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1310                    infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1311
1312                    for instrument in searchResults[iType].values():
1313                        info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format(
1314                            instrument["type"],
1315                            instrument["ticker"],
1316                            "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"],  # right trim for a long string
1317                            instrument["figi"],
1318                        ))
1319
1320                    if iTypeValuesCount <= 5:
1321                        infoShort.extend(info[-iTypeValuesCount:])
1322
1323                    else:
1324                        infoShort.extend(info[-5:])
1325                        infoShort.append(skippedLine)
1326
1327        infoText = "".join(info)
1328        infoTextShort = "".join(infoShort)
1329
1330        if show:
1331            uLogger.info(infoTextShort)
1332            uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`")
1333
1334        if self.searchResultsFile:
1335            with open(self.searchResultsFile, "w", encoding="UTF-8") as fH:
1336                fH.write(infoText)
1337
1338            uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile)))
1339
1340            if self.useHTMLReports:
1341                htmlFilePath = self.searchResultsFile.replace(".md", ".html") if self.searchResultsFile.endswith(".md") else self.searchResultsFile + ".html"
1342                with open(htmlFilePath, "w", encoding="UTF-8") as fH:
1343                    fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Search results", commonCSS=COMMON_CSS, markdown=infoText))
1344
1345                uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
1346
1347        return searchResults

This method search and show information about instruments by part of its ticker, FIGI or name. If searchResultsFile string is not empty then also save information to this file.

Parameters
  • pattern: string with part of ticker, FIGI or instrument's name.
  • show: if True then print results to console, if False — return list of result only.
Returns

list of dictionaries with all found instruments.

def GetUniqueFIGIs(self, instruments: list[str]) -> list:
1349    def GetUniqueFIGIs(self, instruments: list[str]) -> list:
1350        """
1351        Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs.
1352
1353        :param instruments: list of strings with tickers or FIGIs.
1354        :return: list with unique instrument FIGIs only.
1355        """
1356        requestedInstruments = []
1357        for iName in instruments:
1358            if iName not in self.aliases.keys():
1359                if iName not in requestedInstruments:
1360                    requestedInstruments.append(iName)
1361
1362            else:
1363                if iName not in requestedInstruments:
1364                    if self.aliases[iName] not in requestedInstruments:
1365                        requestedInstruments.append(self.aliases[iName])
1366
1367        uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments))
1368
1369        onlyUniqueFIGIs = []
1370        for iName in requestedInstruments:
1371            if iName in TKS_TICKERS_OR_FIGI_EXCLUDED:
1372                continue
1373
1374            self._ticker = iName
1375            iData = self.SearchByTicker(requestPrice=False)  # trying to find instrument by ticker
1376
1377            if not iData:
1378                self._ticker = ""
1379                self._figi = iName
1380
1381                iData = self.SearchByFIGI(requestPrice=False)  # trying to find instrument by FIGI
1382
1383                if not iData:
1384                    self._figi = ""
1385                    uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName))
1386
1387            if iData and iData["figi"] not in onlyUniqueFIGIs:
1388                onlyUniqueFIGIs.append(iData["figi"])
1389
1390        uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs))
1391
1392        return onlyUniqueFIGIs

Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs.

Parameters
  • instruments: list of strings with tickers or FIGIs.
Returns

list with unique instrument FIGIs only.

def GetListOfPrices(self, instruments: list[str], show: bool = False) -> list[dict]:
1394    def GetListOfPrices(self, instruments: list[str], show: bool = False) -> list[dict]:
1395        """
1396        This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation!
1397
1398        See limits: https://tinkoff.github.io/investAPI/limits/
1399
1400        If `pricesFile` string is not empty then also save information to this file.
1401
1402        :param instruments: list of strings with tickers or FIGIs.
1403        :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`.
1404        :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1405                 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods.
1406        """
1407        if instruments is None or not instruments:
1408            uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!")
1409            raise Exception("Ticker or FIGI required")
1410
1411        onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments)
1412
1413        uLogger.debug("Requesting current prices from Tinkoff Broker server...")
1414
1415        iList = []  # trying to get info and current prices about all unique instruments:
1416        for self._figi in onlyUniqueFIGIs:
1417            iData = self.SearchByFIGI(requestPrice=True)
1418            iList.append(iData)
1419
1420        self.ShowListOfPrices(iList, show)
1421
1422        return iList

This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation!

See limits: https://tinkoff.github.io/investAPI/limits/

If pricesFile string is not empty then also save information to this file.

Parameters
  • instruments: list of strings with tickers or FIGIs.
  • show: if True then prints prices to console, if False — prints only to file pricesFile.
Returns

list of instruments looks like [{some ticker info, "currentPrice": {current prices}}, {...}, ...]. One item is dict returned by SearchByTicker() or SearchByFIGI() methods.

def ShowListOfPrices(self, iList: list, show: bool = True) -> str:
1424    def ShowListOfPrices(self, iList: list, show: bool = True) -> str:
1425        """
1426        Show table contains current prices of given instruments.
1427
1428        :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1429                      One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods.
1430        :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`.
1431        :return: multilines text in Markdown format as a table contains current prices.
1432        """
1433        infoText = ""
1434
1435        if show or self.pricesFile:
1436            info = [
1437                "# Current prices\n\n* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
1438                "| Ticker       | FIGI         | Type       | Prev. close | Last price  | Chg. %   | Day limits min/max  | Actual sell / buy   | Curr. |\n",
1439                "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n",
1440            ]
1441
1442            for item in iList:
1443                info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format(
1444                    item["ticker"],
1445                    item["figi"],
1446                    item["type"],
1447                    "{:.2f}".format(float(item["currentPrice"]["closePrice"])),
1448                    "{:.2f}".format(float(item["currentPrice"]["lastPrice"])),
1449                    "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])),
1450                    "{} / {}".format(
1451                        item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A",
1452                        item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A",
1453                    ),
1454                    "{} / {}".format(
1455                        item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A",
1456                        item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A",
1457                    ),
1458                    item["currency"],
1459                ))
1460
1461            infoText = "".join(info)
1462
1463            if show:
1464                uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText))
1465
1466            if self.pricesFile:
1467                with open(self.pricesFile, "w", encoding="UTF-8") as fH:
1468                    fH.write(infoText)
1469
1470                uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile)))
1471
1472                if self.useHTMLReports:
1473                    htmlFilePath = self.pricesFile.replace(".md", ".html") if self.pricesFile.endswith(".md") else self.pricesFile + ".html"
1474                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
1475                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Current prices", commonCSS=COMMON_CSS, markdown=infoText))
1476
1477                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
1478
1479        return infoText

Show table contains current prices of given instruments.

Parameters
  • **iList: list of instruments looks like [{some ticker info, "currentPrice"**: {current prices}}, {...}, ...]. One item is dict returned by SearchByTicker(requestPrice=True) or by SearchByFIGI(requestPrice=True) methods.
  • show: if True then prints prices to console, if False — prints only to file pricesFile.
Returns

multilines text in Markdown format as a table contains current prices.

def RequestTradingStatus(self) -> dict:
1481    def RequestTradingStatus(self) -> dict:
1482        """
1483        Requesting trading status for the instrument defined by `figi` variable.
1484
1485        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus
1486
1487        Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest
1488
1489        :return: dictionary with trading status attributes. Response example:
1490                 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING",
1491                  "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}`
1492        """
1493        if self._figi is None or not self._figi:
1494            uLogger.error("Variable `figi` must be defined for using this method!")
1495            raise Exception("FIGI required")
1496
1497        uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self._figi))
1498
1499        self.body = str({"figi": self._figi, "instrumentId": self._figi})
1500        tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus"
1501        tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST")
1502
1503        if self.moreDebug:
1504            uLogger.debug("Records about current trading status successfully received")
1505
1506        return tradingStatus

Requesting trading status for the instrument defined by figi variable.

REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus

Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest

Returns

dictionary with trading status attributes. Response example: {"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}

def RequestPortfolio(self) -> dict:
1508    def RequestPortfolio(self) -> dict:
1509        """
1510        Requesting actual user's portfolio for current `accountId`.
1511
1512        REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio
1513
1514        Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest
1515
1516        :return: dictionary with user's portfolio.
1517        """
1518        if self.accountId is None or not self.accountId:
1519            uLogger.error("Variable `accountId` must be defined for using this method!")
1520            raise Exception("Account ID required")
1521
1522        uLogger.debug("Requesting current actual user's portfolio. Wait, please...")
1523
1524        self.body = str({"accountId": self.accountId})
1525        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio"
1526        rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST")
1527
1528        if self.moreDebug:
1529            uLogger.debug("Records about user's portfolio successfully received")
1530
1531        return rawPortfolio

Requesting actual user's portfolio for current accountId.

REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio

Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest

Returns

dictionary with user's portfolio.

def RequestPositions(self) -> dict:
1533    def RequestPositions(self) -> dict:
1534        """
1535        Requesting open positions by currencies and instruments for current `accountId`.
1536
1537        REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions
1538
1539        Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest
1540
1541        :return: dictionary with open positions by instruments.
1542        """
1543        if self.accountId is None or not self.accountId:
1544            uLogger.error("Variable `accountId` must be defined for using this method!")
1545            raise Exception("Account ID required")
1546
1547        uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...")
1548
1549        self.body = str({"accountId": self.accountId})
1550        positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions"
1551        rawPositions = self.SendAPIRequest(positionsURL, reqType="POST")
1552
1553        if self.moreDebug:
1554            uLogger.debug("Records about current open positions successfully received")
1555
1556        return rawPositions

Requesting open positions by currencies and instruments for current accountId.

REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions

Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest

Returns

dictionary with open positions by instruments.

def RequestPendingOrders(self) -> list:
1558    def RequestPendingOrders(self) -> list:
1559        """
1560        Requesting current actual pending limit orders for current `accountId`.
1561
1562        REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders
1563
1564        Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest
1565
1566        :return: list of dictionaries with pending limit orders.
1567        """
1568        if self.accountId is None or not self.accountId:
1569            uLogger.error("Variable `accountId` must be defined for using this method!")
1570            raise Exception("Account ID required")
1571
1572        uLogger.debug("Requesting current actual pending limit orders. Wait, please...")
1573
1574        self.body = str({"accountId": self.accountId})
1575        ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders"
1576        rawResponse = self.SendAPIRequest(ordersURL, reqType="POST")
1577
1578        if "orders" in rawResponse.keys():
1579            rawOrders = rawResponse["orders"]
1580            uLogger.debug("[{}] records about pending limit orders received".format(len(rawOrders)))
1581
1582        else:
1583            rawOrders = []
1584            uLogger.debug("No pending limit orders returned! rawResponse = {}".format(rawResponse))
1585
1586        return rawOrders

Requesting current actual pending limit orders for current accountId.

REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders

Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest

Returns

list of dictionaries with pending limit orders.

def RequestStopOrders(self) -> list:
1588    def RequestStopOrders(self) -> list:
1589        """
1590        Requesting current actual stop orders for current `accountId`.
1591
1592        REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders
1593
1594        Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest
1595
1596        :return: list of dictionaries with stop orders.
1597        """
1598        if self.accountId is None or not self.accountId:
1599            uLogger.error("Variable `accountId` must be defined for using this method!")
1600            raise Exception("Account ID required")
1601
1602        uLogger.debug("Requesting current actual stop orders. Wait, please...")
1603
1604        self.body = str({"accountId": self.accountId})
1605        stopOrdersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders"
1606        rawResponse = self.SendAPIRequest(stopOrdersURL, reqType="POST")
1607
1608        if "stopOrders" in rawResponse.keys():
1609            rawStopOrders = rawResponse["stopOrders"]
1610            uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders)))
1611
1612        else:
1613            rawStopOrders = []
1614            uLogger.debug("No stop orders returned! rawResponse = {}".format(rawResponse))
1615
1616        return rawStopOrders

Requesting current actual stop orders for current accountId.

REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders

Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest

Returns

list of dictionaries with stop orders.

def Overview(self, show: bool = False, details: str = 'full') -> dict:
1618    def Overview(self, show: bool = False, details: str = "full") -> dict:
1619        """
1620        Get portfolio: all open positions, orders and some statistics for current `accountId`.
1621        If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile`
1622        and `overviewBondsCalendarFile` are defined then also save information to file.
1623
1624        WARNING! It is not recommended to run this method too many times in a loop! The server receives
1625        many requests about the state of the portfolio, and then, based on the received data, a large number
1626        of calculation and statistics are collected.
1627
1628        :param show: if `False` then only dictionary returns, if `True` then show more debug information.
1629        :param details: how detailed should the information be?
1630        - `full` — shows full available information about portfolio status (by default),
1631        - `positions` — shows only open positions,
1632        - `orders` — shows only sections of open limits and stop orders.
1633        - `digest` — show a short digest of the portfolio status,
1634        - `analytics` — shows only the analytics section and the distribution of the portfolio by various categories,
1635        - `calendar` — shows only the bonds calendar section (if these present in portfolio),
1636        :return: dictionary with client's raw portfolio and some statistics.
1637        """
1638        if self.accountId is None or not self.accountId:
1639            uLogger.error("Variable `accountId` must be defined for using this method!")
1640            raise Exception("Account ID required")
1641
1642        view = {
1643            "raw": {  # --- raw portfolio responses from broker with user portfolio data:
1644                "headers": {},  # list of dictionaries, response headers without "positions" section
1645                "Currencies": [],  # list of dictionaries, open trades with currencies from "positions" section
1646                "Shares": [],  # list of dictionaries, open trades with shares from "positions" section
1647                "Bonds": [],  # list of dictionaries, open trades with bonds from "positions" section
1648                "Etfs": [],  # list of dictionaries, open trades with etfs from "positions" section
1649                "Futures": [],  # list of dictionaries, open trades with futures from "positions" section
1650                "positions": {},  # raw response from broker: dictionary with current available or blocked currencies and instruments for client
1651                "orders": [],  # raw response from broker: list of dictionaries with all pending (market) orders
1652                "stopOrders": [],  # raw response from broker: list of dictionaries with all stop orders
1653                "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}},  # dict with prices of all currencies in RUB
1654            },
1655            "stat": {  # --- some statistics calculated using "raw" sections:
1656                "portfolioCostRUB": 0.,  # portfolio cost in RUB (Russian Rouble)
1657                "availableRUB": 0.,  # available rubles (without other currencies)
1658                "blockedRUB": 0.,  # blocked sum in Russian Rouble
1659                "totalChangesRUB": 0.,  # changes for all open trades in RUB
1660                "totalChangesPercentRUB": 0.,  # changes for all open trades in percents
1661                "allCurrenciesCostRUB": 0.,  # costs of all currencies (include rubles) in RUB
1662                "sharesCostRUB": 0.,  # costs of all shares in RUB
1663                "bondsCostRUB": 0.,  # costs of all bonds in RUB
1664                "etfsCostRUB": 0.,  # costs of all etfs in RUB
1665                "futuresCostRUB": 0.,  # costs of all futures in RUB
1666                "Currencies": [],  # list of dictionaries of all currencies statistics
1667                "Shares": [],  # list of dictionaries of all shares statistics
1668                "Bonds": [],  # list of dictionaries of all bonds statistics
1669                "Etfs": [],  # list of dictionaries of all etfs statistics
1670                "Futures": [],  # list of dictionaries of all futures statistics
1671                "orders": [],  # list of dictionaries of all pending (market) orders and it's parameters
1672                "stopOrders": [],  # list of dictionaries of all stop orders and it's parameters
1673                "blockedCurrencies": {},  # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21}
1674                "blockedInstruments": {},  # dict with blocked  by FIGI, e.g. {}
1675                "funds": {},  # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1676            },
1677            "analytics": {  # --- some analytics of portfolio:
1678                "distrByAssets": {},  # portfolio distribution by assets
1679                "distrByCompanies": {},  # portfolio distribution by companies
1680                "distrBySectors": {},  # portfolio distribution by sectors
1681                "distrByCurrencies": {},  # portfolio distribution by currencies
1682                "distrByCountries": {},  # portfolio distribution by countries
1683                "bondsCalendar": None,  # bonds payment calendar as Pandas DataFrame (if these present in portfolio)
1684            }
1685        }
1686
1687        details = details.lower()
1688        availableDetails = ["full", "positions", "orders", "analytics", "calendar", "digest"]
1689        if details not in availableDetails:
1690            details = "full"
1691            uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails))
1692
1693        uLogger.debug("Requesting portfolio of a client. Wait, please...")
1694
1695        portfolioResponse = self.RequestPortfolio()  # current user's portfolio (dict)
1696        view["raw"]["positions"] = self.RequestPositions()  # current open positions by instruments (dict)
1697        view["raw"]["orders"] = self.RequestPendingOrders()  # current actual pending limit orders (list)
1698        view["raw"]["stopOrders"] = self.RequestStopOrders()  # current actual stop orders (list)
1699
1700        # save response headers without "positions" section:
1701        for key in portfolioResponse.keys():
1702            if key != "positions":
1703                view["raw"]["headers"][key] = portfolioResponse[key]
1704
1705            else:
1706                continue
1707
1708        # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation
1709        # Type of instrument must be only one of supported types in TKS_INSTRUMENTS
1710        for item in portfolioResponse["positions"]:
1711            if item["instrumentType"] == "currency":
1712                self._figi = item["figi"]
1713                if not self._figi and item["ticker"]:
1714                    self._ticker = item["ticker"]
1715                    self._figi = self.SearchByTicker()["figi"]  # Get FIGI to avoid warnings
1716
1717                curr = self.SearchByFIGI(requestPrice=False)
1718
1719                # current price of currency in RUB:
1720                view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = {
1721                    "name": curr["name"],
1722                    "currentPrice": NanoToFloat(
1723                        item["currentPrice"]["units"],
1724                        item["currentPrice"]["nano"]
1725                    ),
1726                }
1727
1728                view["raw"]["Currencies"].append(item)
1729
1730            elif item["instrumentType"] == "share":
1731                view["raw"]["Shares"].append(item)
1732
1733            elif item["instrumentType"] == "bond":
1734                view["raw"]["Bonds"].append(item)
1735
1736            elif item["instrumentType"] == "etf":
1737                view["raw"]["Etfs"].append(item)
1738
1739            elif item["instrumentType"] == "futures":
1740                view["raw"]["Futures"].append(item)
1741
1742            else:
1743                continue
1744
1745        # how many volume of currencies (by ISO currency name) are blocked:
1746        for item in view["raw"]["positions"]["blocked"]:
1747            blocked = NanoToFloat(item["units"], item["nano"])
1748            if blocked > 0:
1749                view["stat"]["blockedCurrencies"][item["currency"]] = blocked
1750
1751        # how many volume of instruments (by FIGI) are blocked:
1752        for item in view["raw"]["positions"]["securities"]:
1753            blocked = int(item["blocked"])
1754            if blocked > 0:
1755                view["stat"]["blockedInstruments"][item["figi"]] = blocked
1756
1757        allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]}
1758
1759        if "rub" in allBlocked.keys():
1760            view["stat"]["blockedRUB"] = allBlocked["rub"]  # blocked rubles
1761
1762        # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies:
1763        view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"])
1764        view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"])
1765        view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"])
1766        view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"])
1767        view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"])
1768        view["stat"]["portfolioCostRUB"] = sum([
1769            view["stat"]["allCurrenciesCostRUB"],
1770            view["stat"]["sharesCostRUB"],
1771            view["stat"]["bondsCostRUB"],
1772            view["stat"]["etfsCostRUB"],
1773            view["stat"]["futuresCostRUB"],
1774        ])
1775
1776        # --- calculating some portfolio statistics:
1777        byComp = {}  # distribution by companies
1778        bySect = {}  # distribution by sectors
1779        byCurr = {}  # distribution by currencies (include RUB)
1780        unknownCountryName = "All other countries"  # default name for instruments without "countryOfRisk" and "countryOfRiskName"
1781        byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}}  # distribution by countries (currencies are included in their countries)
1782
1783        for item in portfolioResponse["positions"]:
1784            self._figi = item["figi"]
1785            if not self._figi and item["ticker"]:
1786                self._ticker = item["ticker"]
1787                self._figi = self.SearchByTicker()["figi"]  # Get FIGI to avoid warnings
1788
1789            instrument = self.SearchByFIGI(requestPrice=False)  # full raw info about instrument by FIGI
1790
1791            if instrument:
1792                if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys():
1793                    blocked = allBlocked[instrument["nominal"]["currency"]]  # blocked volume of currency
1794
1795                elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys():
1796                    blocked = allBlocked[item["figi"]]  # blocked volume of other instruments
1797
1798                else:
1799                    blocked = 0
1800
1801                volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"])  # available volume of instrument
1802                lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"])  # available volume in lots of instrument
1803                direction = "Long" if lots >= 0 else "Short"  # direction of an instrument's position: short or long
1804                curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"])  # current instrument's price
1805                average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"])  # current average position price
1806                profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"])  # expected profit at current moment
1807                currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"]  # currency name rub, usd, eur etc.
1808                cost = curPrice if "currentNkd" not in item.keys() else (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume  # current cost of all volume of instrument in basic asset
1809                baseCurrencyName = item["currentPrice"]["currency"]  # name of base currency (rub)
1810                countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName
1811                costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"]  # cost in rubles
1812                percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.  # instrument's part in percent of full portfolio cost
1813
1814                statData = {
1815                    "figi": item["figi"],  # FIGI from REST API "GetPortfolio" method
1816                    "ticker": instrument["ticker"],  # ticker by FIGI
1817                    "currency": currency,  # currency name rub, usd, eur etc. for instrument price
1818                    "volume": volume,  # available volume of instrument
1819                    "lots": lots,  # volume in lots of instrument
1820                    "direction": direction,  # direction of an instrument's position: short or long
1821                    "blocked": blocked,  # blocked volume of currency or instrument
1822                    "currentPrice": curPrice,  # current instrument's price in basic asset
1823                    "average": average,  # current average position price
1824                    "cost": cost,  # current cost of all volume of instrument in basic asset
1825                    "baseCurrencyName": baseCurrencyName,  # name of base currency (rub)
1826                    "costRUB": costRUB,  # cost of instrument in ruble
1827                    "percentCostRUB": percentCostRUB,  # instrument's part in percent of full portfolio cost in RUB
1828                    "profit": profit,  # expected profit at current moment
1829                    "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0,  # expected percents of profit at current moment for this instrument
1830                    "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other",
1831                    "name": instrument["name"] if "name" in instrument.keys() else "",  # human-readable names of instruments
1832                    "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "",  # ISO name for currencies only
1833                    "country": countryName,  # e.g. "[RU] Российская Федерация" or unknownCountryName
1834                    "step": instrument["step"],  # minimum price increment
1835                }
1836
1837                # adding distribution by unique countries:
1838                if statData["country"] not in byCountry.keys():
1839                    byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB}
1840
1841                else:
1842                    byCountry[statData["country"]]["cost"] += costRUB
1843                    byCountry[statData["country"]]["percent"] += percentCostRUB
1844
1845                if item["instrumentType"] != "currency":
1846                    # adding distribution by unique companies:
1847                    if statData["name"]:
1848                        if statData["name"] not in byComp.keys():
1849                            byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB}
1850
1851                        else:
1852                            byComp[statData["name"]]["cost"] += costRUB
1853                            byComp[statData["name"]]["percent"] += percentCostRUB
1854
1855                    # adding distribution by unique sectors:
1856                    if statData["sector"] not in bySect.keys():
1857                        bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB}
1858
1859                    else:
1860                        bySect[statData["sector"]]["cost"] += costRUB
1861                        bySect[statData["sector"]]["percent"] += percentCostRUB
1862
1863                # adding distribution by unique currencies:
1864                if currency not in byCurr.keys():
1865                    byCurr[currency] = {
1866                        "name": view["raw"]["currenciesCurrentPrices"][currency]["name"],
1867                        "cost": costRUB,
1868                        "percent": percentCostRUB
1869                    }
1870
1871                else:
1872                    byCurr[currency]["cost"] += costRUB
1873                    byCurr[currency]["percent"] += percentCostRUB
1874
1875                # saving statistics for every instrument:
1876                if item["instrumentType"] == "currency":
1877                    view["stat"]["Currencies"].append(statData)
1878
1879                    # update dict with free funds for trading (total - blocked) by currencies
1880                    # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1881                    view["stat"]["funds"][currency] = {
1882                        "total": volume,
1883                        "totalCostRUB": costRUB,  # total volume cost in rubles
1884                        "free": volume - blocked,
1885                        "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0,  # free volume cost in rubles
1886                    }
1887
1888                elif item["instrumentType"] == "share":
1889                    view["stat"]["Shares"].append(statData)
1890
1891                elif item["instrumentType"] == "bond":
1892                    view["stat"]["Bonds"].append(statData)
1893
1894                elif item["instrumentType"] == "etf":
1895                    view["stat"]["Etfs"].append(statData)
1896
1897                elif item["instrumentType"] == "Futures":
1898                    view["stat"]["Futures"].append(statData)
1899
1900                else:
1901                    continue
1902
1903        # total changes in Russian Ruble:
1904        view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]])  # available RUB without other currencies
1905        view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0.
1906        startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100)
1907        view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost
1908        view["stat"]["funds"]["rub"] = {
1909            "total": view["stat"]["availableRUB"],
1910            "totalCostRUB": view["stat"]["availableRUB"],
1911            "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1912            "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1913        }
1914
1915        # --- pending limit orders sector data:
1916        uniquePendingOrdersFIGIs = []  # unique FIGIs of pending limit orders to avoid many times price requests
1917        uniquePendingOrders = {}  # unique instruments with FIGIs as dictionary keys
1918
1919        for item in view["raw"]["orders"]:
1920            self._figi = item["figi"]
1921
1922            if item["figi"] not in uniquePendingOrdersFIGIs:
1923                instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI, price requests only one time
1924
1925                uniquePendingOrdersFIGIs.append(item["figi"])
1926                uniquePendingOrders[item["figi"]] = instrument
1927
1928            else:
1929                instrument = uniquePendingOrders[item["figi"]]
1930
1931            if instrument:
1932                action = TKS_ORDER_DIRECTIONS[item["direction"]]
1933                orderType = TKS_ORDER_TYPES[item["orderType"]]
1934                orderState = TKS_ORDER_STATES[item["executionReportStatus"]]
1935                orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1936
1937                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1938                if item["direction"] == "ORDER_DIRECTION_BUY":
1939                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1940
1941                else:
1942                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
1943
1944                # requested price for order execution:
1945                target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"])
1946
1947                # necessary changes in percent to reach target from current price:
1948                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
1949
1950                view["stat"]["orders"].append({
1951                    "orderID": item["orderId"],  # orderId number parameter of current order
1952                    "figi": item["figi"],  # FIGI identification
1953                    "ticker": instrument["ticker"],  # ticker name by FIGI
1954                    "lotsRequested": item["lotsRequested"],  # requested lots value
1955                    "lotsExecuted": item["lotsExecuted"],  # how many lots are executed
1956                    "currentPrice": lastPrice,  # current instrument's price for defined action
1957                    "targetPrice": target,  # requested price for order execution in base currency
1958                    "baseCurrencyName": item["initialSecurityPrice"]["currency"],  # name of base currency
1959                    "percentChanges": changes,  # changes in percent to target from current price
1960                    "currency": item["currency"],  # instrument's currency name
1961                    "action": action,  # sell / buy / Unknown from TKS_ORDER_DIRECTIONS
1962                    "type": orderType,  # type of order from TKS_ORDER_TYPES
1963                    "status": orderState,  # order status from TKS_ORDER_STATES
1964                    "date": orderDate,  # string with order date and time from UTC format (without nano seconds part)
1965                })
1966
1967        # --- stop orders sector data:
1968        uniqueStopOrdersFIGIs = []  # unique FIGIs of stop orders to avoid many times price requests
1969        uniqueStopOrders = {}  # unique instruments with FIGIs as dictionary keys
1970
1971        for item in view["raw"]["stopOrders"]:
1972            self._figi = item["figi"]
1973
1974            if item["figi"] not in uniqueStopOrdersFIGIs:
1975                instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI, price requests only one time
1976
1977                uniqueStopOrdersFIGIs.append(item["figi"])
1978                uniqueStopOrders[item["figi"]] = instrument
1979
1980            else:
1981                instrument = uniqueStopOrders[item["figi"]]
1982
1983            if instrument:
1984                action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]]
1985                orderType = TKS_STOP_ORDER_TYPES[item["orderType"]]
1986                createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1987
1988                # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order
1989                if "expirationTime" in item.keys():
1990                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"]
1991                    expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0]
1992
1993                else:
1994                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"]
1995                    expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"]
1996
1997                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1998                if item["direction"] == "STOP_ORDER_DIRECTION_BUY":
1999                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
2000
2001                else:
2002                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
2003
2004                # requested price when stop-order executed:
2005                target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"])
2006
2007                # price for limit-order, set up when stop-order executed:
2008                limit = NanoToFloat(item["price"]["units"], item["price"]["nano"])
2009
2010                # necessary changes in percent to reach target from current price:
2011                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
2012
2013                view["stat"]["stopOrders"].append({
2014                    "orderID": item["stopOrderId"],  # stopOrderId number parameter of current stop-order
2015                    "figi": item["figi"],  # FIGI identification
2016                    "ticker": instrument["ticker"],  # ticker name by FIGI
2017                    "lotsRequested": item["lotsRequested"],  # requested lots value
2018                    "currentPrice": lastPrice,  # current instrument's price for defined action
2019                    "targetPrice": target,  # requested price for stop-order execution in base currency
2020                    "limitPrice": limit,  # price for limit-order, set up when stop-order executed, 0 if market order
2021                    "baseCurrencyName": item["stopPrice"]["currency"],  # name of base currency
2022                    "percentChanges": changes,  # changes in percent to target from current price
2023                    "currency": item["currency"],  # instrument's currency name
2024                    "action": action,  # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS
2025                    "type": orderType,  # type of order from TKS_STOP_ORDER_TYPES
2026                    "expType": expType,  # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES
2027                    "createDate": createDate,  # string with created order date and time from UTC format (without nano seconds part)
2028                    "expDate": expDate,  # string with expiration order date and time from UTC format (without nano seconds part)
2029                })
2030
2031        # --- calculating data for analytics section:
2032        # portfolio distribution by assets:
2033        view["analytics"]["distrByAssets"] = {
2034            "Ruble": {
2035                "uniques": 1,
2036                "cost": view["stat"]["availableRUB"],
2037                "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2038            },
2039            "Currencies": {
2040                "uniques": len(view["stat"]["Currencies"]),  # all foreign currencies without RUB
2041                "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"],
2042                "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2043            },
2044            "Shares": {
2045                "uniques": len(view["stat"]["Shares"]),
2046                "cost": view["stat"]["sharesCostRUB"],
2047                "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2048            },
2049            "Bonds": {
2050                "uniques": len(view["stat"]["Bonds"]),
2051                "cost": view["stat"]["bondsCostRUB"],
2052                "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2053            },
2054            "Etfs": {
2055                "uniques": len(view["stat"]["Etfs"]),
2056                "cost": view["stat"]["etfsCostRUB"],
2057                "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2058            },
2059            "Futures": {
2060                "uniques": len(view["stat"]["Futures"]),
2061                "cost": view["stat"]["futuresCostRUB"],
2062                "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2063            },
2064        }
2065
2066        # portfolio distribution by companies:
2067        view["analytics"]["distrByCompanies"]["All money cash"] = {
2068            "ticker": "",
2069            "cost": view["stat"]["allCurrenciesCostRUB"],
2070            "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2071        }
2072        view["analytics"]["distrByCompanies"].update(byComp)
2073
2074        # portfolio distribution by sectors:
2075        view["analytics"]["distrBySectors"]["All money cash"] = {
2076            "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"],
2077            "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"],
2078        }
2079        view["analytics"]["distrBySectors"].update(bySect)
2080
2081        # portfolio distribution by currencies:
2082        if "rub" not in view["analytics"]["distrByCurrencies"].keys():
2083            view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0}
2084
2085            if self.moreDebug:
2086                uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!")
2087
2088        view["analytics"]["distrByCurrencies"].update(byCurr)
2089        view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2090        view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2091
2092        # portfolio distribution by countries:
2093        if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys():
2094            view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0}
2095
2096            if self.moreDebug:
2097                uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!")
2098
2099        view["analytics"]["distrByCountries"].update(byCountry)
2100        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2101        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2102
2103        # --- Prepare text statistics overview in human-readable:
2104        if show:
2105            actualOnDate = datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)
2106
2107            # Whatever the value `details`, header not changes:
2108            info = [
2109                "# Client's portfolio\n\n",
2110                "* **Actual on date:** [{} UTC]\n".format(actualOnDate),
2111                "* **Account ID:** [{}]\n".format(self.accountId),
2112            ]
2113
2114            if details in ["full", "positions", "digest"]:
2115                info.extend([
2116                    "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2117                    "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format(
2118                        "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2119                        view["stat"]["totalChangesRUB"],
2120                        "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2121                        view["stat"]["totalChangesPercentRUB"],
2122                    ),
2123                ])
2124
2125            if details in ["full", "positions"]:
2126                info.extend([
2127                    "## Open positions\n\n",
2128                    "| Ticker [FIGI]               | Volume (blocked)                | Lots     | Curr. price  | Avg. price   | Current volume cost | Profit (%)                   |\n",
2129                    "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n",
2130                    "| **Ruble:**                  | {:>31} |          |              |              |                     |                              |\n".format(
2131                        "{:.2f} ({:.2f}) rub".format(
2132                            view["stat"]["availableRUB"],
2133                            view["stat"]["blockedRUB"],
2134                        )
2135                    )
2136                ])
2137
2138                def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list:
2139                    return [
2140                        "|                             |                                 |          |              |              |                     |                              |\n",
2141                        "| {:<27} |                                 |          |              |              | {:>19} |                              |\n".format(
2142                            noTradeStr if noTradeStr else typeStr,
2143                            "" if noTradeStr else "{:.2f} RUB".format(CostRUB),
2144                        ),
2145                    ]
2146
2147                def _InfoStr(data: dict, isCurr: bool = False) -> str:
2148                    return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format(
2149                        "{} [{}]".format(data["ticker"], data["figi"]),
2150                        "{:.2f} ({:.2f}) {}".format(
2151                            data["volume"],
2152                            data["blocked"],
2153                            data["currency"],
2154                        ) if isCurr else "{:.0f} ({:.0f})".format(
2155                            data["volume"],
2156                            data["blocked"],
2157                        ),
2158                        "—" if isCurr else "{:.4f}".format(data["lots"]).rstrip("0").rstrip("."),
2159                        "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a",
2160                        "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a",
2161                        "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]),
2162                        "{}{:.2f} {} ({}{:.2f}%)".format(
2163                            "+" if data["profit"] > 0 else "",
2164                            data["profit"], data["baseCurrencyName"],
2165                            "+" if data["percentProfit"] > 0 else "",
2166                            data["percentProfit"],
2167                        ),
2168                    )
2169
2170                # --- Show currencies section:
2171                if view["stat"]["Currencies"]:
2172                    info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**"))
2173                    for item in view["stat"]["Currencies"]:
2174                        info.append(_InfoStr(item, isCurr=True))
2175
2176                else:
2177                    info.extend(_SplitStr(noTradeStr="**Currencies:** no trades"))
2178
2179                # --- Show shares section:
2180                if view["stat"]["Shares"]:
2181                    info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**"))
2182
2183                    for item in view["stat"]["Shares"]:
2184                        info.append(_InfoStr(item))
2185
2186                else:
2187                    info.extend(_SplitStr(noTradeStr="**Shares:** no trades"))
2188
2189                # --- Show bonds section:
2190                if view["stat"]["Bonds"]:
2191                    info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**"))
2192
2193                    for item in view["stat"]["Bonds"]:
2194                        info.append(_InfoStr(item))
2195
2196                else:
2197                    info.extend(_SplitStr(noTradeStr="**Bonds:** no trades"))
2198
2199                # --- Show etfs section:
2200                if view["stat"]["Etfs"]:
2201                    info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**"))
2202
2203                    for item in view["stat"]["Etfs"]:
2204                        info.append(_InfoStr(item))
2205
2206                else:
2207                    info.extend(_SplitStr(noTradeStr="**Etfs:** no trades"))
2208
2209                # --- Show futures section:
2210                if view["stat"]["Futures"]:
2211                    info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**"))
2212
2213                    for item in view["stat"]["Futures"]:
2214                        info.append(_InfoStr(item))
2215
2216                else:
2217                    info.extend(_SplitStr(noTradeStr="**Futures:** no trades"))
2218
2219            if details in ["full", "orders"]:
2220                # --- Show pending limit orders section:
2221                if view["stat"]["orders"]:
2222                    info.extend([
2223                        "\n## Opened pending limit-orders: [{}]\n".format(len(view["stat"]["orders"])),
2224                        "\n| Ticker [FIGI]               | Order ID       | Lots (exec.) | Current price (% delta) | Target price  | Action    | Type      | Create date (UTC)       |\n",
2225                        "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n",
2226                    ])
2227
2228                    for item in view["stat"]["orders"]:
2229                        info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format(
2230                            "{} [{}]".format(item["ticker"], item["figi"]),
2231                            item["orderID"],
2232                            "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]),
2233                            "{} {} ({}{:.2f}%)".format(
2234                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2235                                item["baseCurrencyName"],
2236                                "+" if item["percentChanges"] > 0 else "",
2237                                float(item["percentChanges"]),
2238                            ),
2239                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2240                            item["action"],
2241                            item["type"],
2242                            item["date"],
2243                        ))
2244
2245                else:
2246                    info.append("\n## Total pending limit-orders: [0]\n")
2247
2248                # --- Show stop orders section:
2249                if view["stat"]["stopOrders"]:
2250                    info.extend([
2251                        "\n## Opened stop-orders: [{}]\n".format(len(view["stat"]["stopOrders"])),
2252                        "\n| Ticker [FIGI]               | Stop order ID                        | Lots   | Current price (% delta) | Target price  | Limit price   | Action    | Type        | Expire type  | Create date (UTC)   | Expiration (UTC)    |\n",
2253                        "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n",
2254                    ])
2255
2256                    for item in view["stat"]["stopOrders"]:
2257                        info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format(
2258                            "{} [{}]".format(item["ticker"], item["figi"]),
2259                            item["orderID"],
2260                            item["lotsRequested"],
2261                            "{} {} ({}{:.2f}%)".format(
2262                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2263                                item["baseCurrencyName"],
2264                                "+" if item["percentChanges"] > 0 else "",
2265                                float(item["percentChanges"]),
2266                            ),
2267                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2268                            "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"],
2269                            item["action"],
2270                            item["type"],
2271                            item["expType"],
2272                            item["createDate"],
2273                            item["expDate"],
2274                        ))
2275
2276                else:
2277                    info.append("\n## Total stop-orders: [0]\n")
2278
2279            if details in ["full", "analytics"]:
2280                # -- Show analytics section:
2281                if view["stat"]["portfolioCostRUB"] > 0:
2282                    info.extend([
2283                        "\n# Analytics\n\n"
2284                        "* **Actual on date:** [{} UTC]\n".format(actualOnDate),
2285                        "* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2286                        "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format(
2287                            "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2288                            view["stat"]["totalChangesRUB"],
2289                            "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2290                            view["stat"]["totalChangesPercentRUB"],
2291                        ),
2292                        "\n## Portfolio distribution by assets\n"
2293                        "\n| Type                               | Uniques | Percent | Current cost       |\n",
2294                        "|------------------------------------|---------|---------|--------------------|\n",
2295                    ])
2296
2297                    for key in view["analytics"]["distrByAssets"].keys():
2298                        if view["analytics"]["distrByAssets"][key]["cost"] > 0:
2299                            info.append("| {:<34} | {:<7} | {:<7} | {:<18} |\n".format(
2300                                key,
2301                                view["analytics"]["distrByAssets"][key]["uniques"],
2302                                "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]),
2303                                "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]),
2304                            ))
2305
2306                    aSepLine = "|----------------------------------------------|---------|--------------------|\n"
2307
2308                    info.extend([
2309                        "\n## Portfolio distribution by companies\n"
2310                        "\n| Company                                      | Percent | Current cost       |\n",
2311                        aSepLine,
2312                    ])
2313
2314                    for company in view["analytics"]["distrByCompanies"].keys():
2315                        if view["analytics"]["distrByCompanies"][company]["cost"] > 0:
2316                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2317                                "{}{}".format(
2318                                    "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "",
2319                                    company,
2320                                ),
2321                                "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]),
2322                                "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]),
2323                            ))
2324
2325                    info.extend([
2326                        "\n## Portfolio distribution by sectors\n"
2327                        "\n| Sector                                       | Percent | Current cost       |\n",
2328                        aSepLine,
2329                    ])
2330
2331                    for sector in view["analytics"]["distrBySectors"].keys():
2332                        if view["analytics"]["distrBySectors"][sector]["cost"] > 0:
2333                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2334                                sector,
2335                                "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]),
2336                                "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]),
2337                            ))
2338
2339                    info.extend([
2340                        "\n## Portfolio distribution by currencies\n"
2341                        "\n| Instruments currencies                       | Percent | Current cost       |\n",
2342                        aSepLine,
2343                    ])
2344
2345                    for curr in view["analytics"]["distrByCurrencies"].keys():
2346                        if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0:
2347                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2348                                "[{}] {}".format(curr, view["analytics"]["distrByCurrencies"][curr]["name"]),
2349                                "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]),
2350                                "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]),
2351                            ))
2352
2353                    info.extend([
2354                        "\n## Portfolio distribution by countries\n"
2355                        "\n| Assets by country                            | Percent | Current cost       |\n",
2356                        aSepLine,
2357                    ])
2358
2359                    for country in view["analytics"]["distrByCountries"].keys():
2360                        if view["analytics"]["distrByCountries"][country]["cost"] > 0:
2361                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2362                                country,
2363                                "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]),
2364                                "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]),
2365                            ))
2366
2367            if details in ["full", "calendar"]:
2368                # -- Show bonds payment calendar section:
2369                if view["stat"]["Bonds"]:
2370                    bondTickers = [item["ticker"] for item in view["stat"]["Bonds"]]
2371                    view["analytics"]["bondsCalendar"] = self.ExtendBondsData(instruments=bondTickers, xlsx=False)
2372                    info.append("\n" + self.ShowBondsCalendar(extBonds=view["analytics"]["bondsCalendar"], show=False))
2373
2374                else:
2375                    info.append("\n# Bond payments calendar\n\nNo bonds in the portfolio to create payments calendar\n")
2376
2377            infoText = "".join(info)
2378
2379            uLogger.info(infoText)
2380
2381            if details == "full" and self.overviewFile:
2382                filename = self.overviewFile
2383
2384            elif details == "digest" and self.overviewDigestFile:
2385                filename = self.overviewDigestFile
2386
2387            elif details == "positions" and self.overviewPositionsFile:
2388                filename = self.overviewPositionsFile
2389
2390            elif details == "orders" and self.overviewOrdersFile:
2391                filename = self.overviewOrdersFile
2392
2393            elif details == "analytics" and self.overviewAnalyticsFile:
2394                filename = self.overviewAnalyticsFile
2395
2396            elif details == "calendar" and self.overviewBondsCalendarFile:
2397                filename = self.overviewBondsCalendarFile
2398
2399            else:
2400                filename = ""
2401
2402            if filename:
2403                with open(filename, "w", encoding="UTF-8") as fH:
2404                    fH.write(infoText)
2405
2406                uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename)))
2407
2408                if self.useHTMLReports:
2409                    htmlFilePath = filename.replace(".md", ".html") if filename.endswith(".md") else filename + ".html"
2410                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
2411                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Client's portfolio", commonCSS=COMMON_CSS, markdown=infoText))
2412
2413                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
2414
2415        return view

Get portfolio: all open positions, orders and some statistics for current accountId. If overviewFile, overviewDigestFile, overviewPositionsFile, overviewOrdersFile, overviewAnalyticsFile and overviewBondsCalendarFile are defined then also save information to file.

WARNING! It is not recommended to run this method too many times in a loop! The server receives many requests about the state of the portfolio, and then, based on the received data, a large number of calculation and statistics are collected.

Parameters
  • show: if False then only dictionary returns, if True then show more debug information.
  • details: how detailed should the information be?
    • full — shows full available information about portfolio status (by default),
    • positions — shows only open positions,
    • orders — shows only sections of open limits and stop orders.
    • digest — show a short digest of the portfolio status,
    • analytics — shows only the analytics section and the distribution of the portfolio by various categories,
    • calendar — shows only the bonds calendar section (if these present in portfolio),
Returns

dictionary with client's raw portfolio and some statistics.

def Deals( self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple[list[dict], dict]:
2417    def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple[list[dict], dict]:
2418        """
2419        Returns history operations between two given dates for current `accountId`.
2420        If `reportFile` string is not empty then also save human-readable report.
2421        Shows some statistical data of closed positions.
2422
2423        :param start: see docstring in `TradeRoutines.GetDatesAsString()` method.
2424        :param end: see docstring in `TradeRoutines.GetDatesAsString()` method.
2425        :param show: if `True` then also prints all records to the console.
2426        :param showCancelled: if `False` then remove information about cancelled operations from the deals report.
2427        :return: original list of dictionaries with history of deals records from API ("operations" key):
2428                 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2429                 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.
2430        """
2431        if self.accountId is None or not self.accountId:
2432            uLogger.error("Variable `accountId` must be defined for using this method!")
2433            raise Exception("Account ID required")
2434
2435        startDate, endDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT)  # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2436
2437        uLogger.debug("Requesting history of a client's operations. Wait, please...")
2438
2439        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2440        dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations"
2441        self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate})
2442        ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"]  # list of dict: operations returns by broker
2443        customStat = {}  # custom statistics in additional to responseJSON
2444
2445        # --- output report in human-readable format:
2446        if show or self.reportFile:
2447            splitLine1 = "|                            |                               |                              |                      |                        |\n"  # Summary section
2448            splitLine2 = "|                     |              |              |            |           |                 |            |                                                                    |\n"  # Operations section
2449            nextDay = ""
2450
2451            info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])]
2452
2453            if len(ops) > 0:
2454                customStat = {
2455                    "opsCount": 0,  # total operations count
2456                    "buyCount": 0,  # buy operations
2457                    "sellCount": 0,  # sell operations
2458                    "buyTotal": {"rub": 0.},  # Buy sums in different currencies
2459                    "sellTotal": {"rub": 0.},  # Sell sums in different currencies
2460                    "payIn": {"rub": 0.},  # Deposit brokerage account
2461                    "payOut": {"rub": 0.},  # Withdrawals
2462                    "divs": {"rub": 0.},  # Dividends income
2463                    "coupons": {"rub": 0.},  # Coupon's income
2464                    "brokerCom": {"rub": 0.},  # Service commissions
2465                    "serviceCom": {"rub": 0.},  # Service commissions
2466                    "marginCom": {"rub": 0.},  # Margin commissions
2467                    "allTaxes": {"rub": 0.},  # Sum of withholding taxes and corrections
2468                }
2469
2470                # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES:
2471                for item in ops:
2472                    if item["state"] == "OPERATION_STATE_EXECUTED":
2473                        payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2474
2475                        # count buy operations:
2476                        if "_BUY" in item["operationType"]:
2477                            customStat["buyCount"] += 1
2478
2479                            if item["payment"]["currency"] in customStat["buyTotal"].keys():
2480                                customStat["buyTotal"][item["payment"]["currency"]] += payment
2481
2482                            else:
2483                                customStat["buyTotal"][item["payment"]["currency"]] = payment
2484
2485                        # count sell operations:
2486                        elif "_SELL" in item["operationType"]:
2487                            customStat["sellCount"] += 1
2488
2489                            if item["payment"]["currency"] in customStat["sellTotal"].keys():
2490                                customStat["sellTotal"][item["payment"]["currency"]] += payment
2491
2492                            else:
2493                                customStat["sellTotal"][item["payment"]["currency"]] = payment
2494
2495                        # count incoming operations:
2496                        elif item["operationType"] in ["OPERATION_TYPE_INPUT"]:
2497                            if item["payment"]["currency"] in customStat["payIn"].keys():
2498                                customStat["payIn"][item["payment"]["currency"]] += payment
2499
2500                            else:
2501                                customStat["payIn"][item["payment"]["currency"]] = payment
2502
2503                        # count withdrawals operations:
2504                        elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]:
2505                            if item["payment"]["currency"] in customStat["payOut"].keys():
2506                                customStat["payOut"][item["payment"]["currency"]] += payment
2507
2508                            else:
2509                                customStat["payOut"][item["payment"]["currency"]] = payment
2510
2511                        # count dividends income:
2512                        elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]:
2513                            if item["payment"]["currency"] in customStat["divs"].keys():
2514                                customStat["divs"][item["payment"]["currency"]] += payment
2515
2516                            else:
2517                                customStat["divs"][item["payment"]["currency"]] = payment
2518
2519                        # count coupon's income:
2520                        elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]:
2521                            if item["payment"]["currency"] in customStat["coupons"].keys():
2522                                customStat["coupons"][item["payment"]["currency"]] += payment
2523
2524                            else:
2525                                customStat["coupons"][item["payment"]["currency"]] = payment
2526
2527                        # count broker commissions:
2528                        elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]:
2529                            if item["payment"]["currency"] in customStat["brokerCom"].keys():
2530                                customStat["brokerCom"][item["payment"]["currency"]] += payment
2531
2532                            else:
2533                                customStat["brokerCom"][item["payment"]["currency"]] = payment
2534
2535                        # count service commissions:
2536                        elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]:
2537                            if item["payment"]["currency"] in customStat["serviceCom"].keys():
2538                                customStat["serviceCom"][item["payment"]["currency"]] += payment
2539
2540                            else:
2541                                customStat["serviceCom"][item["payment"]["currency"]] = payment
2542
2543                        # count margin commissions:
2544                        elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]:
2545                            if item["payment"]["currency"] in customStat["marginCom"].keys():
2546                                customStat["marginCom"][item["payment"]["currency"]] += payment
2547
2548                            else:
2549                                customStat["marginCom"][item["payment"]["currency"]] = payment
2550
2551                        # count withholding taxes:
2552                        elif "_TAX" in item["operationType"]:
2553                            if item["payment"]["currency"] in customStat["allTaxes"].keys():
2554                                customStat["allTaxes"][item["payment"]["currency"]] += payment
2555
2556                            else:
2557                                customStat["allTaxes"][item["payment"]["currency"]] = payment
2558
2559                        else:
2560                            continue
2561
2562                customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"]
2563
2564                # --- view "Actions" lines:
2565                info.extend([
2566                    "| Report sections            |                               |                              |                      |                        |\n",
2567                    "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n",
2568                    "| **Actions:**               | Trades: {:<21} | Trading volumes:             |                      |                        |\n".format(customStat["opsCount"]),
2569                    "|                            |   Buy: {:<22} | {:<28} |                      |                        |\n".format(
2570                        "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2571                        "  rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else "  —",
2572                    ),
2573                    "|                            |   Sell: {:<21} | {:<28} |                      |                        |\n".format(
2574                        "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2575                        "  rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else "  —",
2576                    ),
2577                ])
2578
2579                opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys()))))
2580                for key in opsKeys:
2581                    if key == "rub":
2582                        continue
2583
2584                    info.extend([
2585                        "|                            |                               | {:<28} |                      |                        |\n".format(
2586                            "  {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0)
2587                        ),
2588                        "|                            |                               | {:<28} |                      |                        |\n".format(
2589                            "  {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0)
2590                        ),
2591                    ])
2592
2593                info.append(splitLine1)
2594
2595                def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str:
2596                    return "|                            | {:<29} | {:<28} | {:<20} | {:<22} |\n".format(
2597                            "  {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else "  —",
2598                            "  {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else "  —",
2599                            "  {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else "  —",
2600                            "  {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else "  —",
2601                    )
2602
2603                # --- view "Payments" lines:
2604                info.append("| **Payments:**              | Deposit on broker account:    | Withdrawals:                 | Dividends income:    | Coupons income:        |\n")
2605                paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys()))))
2606
2607                for key in paymentsKeys:
2608                    info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key))
2609
2610                info.append(splitLine1)
2611
2612                # --- view "Commissions and taxes" lines:
2613                info.append("| **Commissions and taxes:** | Broker commissions:           | Service commissions:         | Margin commissions:  | All taxes/corrections: |\n")
2614                comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys()))))
2615
2616                for key in comKeys:
2617                    info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key))
2618
2619                info.extend([
2620                    "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"),
2621                    "| Date and time       | FIGI         | Ticker       | Asset      | Value     | Payment         | Status     | Operation type                                                     |\n",
2622                    "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n",
2623                ])
2624
2625            else:
2626                info.append("Broker returned no operations during this period\n")
2627
2628            # --- view "Operations" section:
2629            for item in ops:
2630                if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]:
2631                    continue
2632
2633                else:
2634                    self._figi = item["figi"]
2635                    payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2636                    instrument = self.SearchByFIGI(requestPrice=False) if self._figi else {}
2637
2638                    # group of deals during one day:
2639                    if nextDay and item["date"].split("T")[0] != nextDay:
2640                        info.append(splitLine2)
2641                        nextDay = ""
2642
2643                    else:
2644                        nextDay = item["date"].split("T")[0]  # saving current day for splitting
2645
2646                    info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format(
2647                        item["date"].replace("T", " ").replace("Z", "").split(".")[0],
2648                        self._figi if self._figi else "—",
2649                        instrument["ticker"] if instrument else "—",
2650                        instrument["type"] if instrument else "—",
2651                        item["quantity"] if int(item["quantity"]) > 0 else "—",
2652                        "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—",
2653                        TKS_OPERATION_STATES[item["state"]],
2654                        TKS_OPERATION_TYPES[item["operationType"]],
2655                    ))
2656
2657            infoText = "".join(info)
2658
2659            if show:
2660                if self.moreDebug:
2661                    uLogger.debug("Records about history of a client's operations successfully received")
2662
2663                uLogger.info(infoText)
2664
2665            if self.reportFile:
2666                with open(self.reportFile, "w", encoding="UTF-8") as fH:
2667                    fH.write(infoText)
2668
2669                uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile)))
2670
2671                if self.useHTMLReports:
2672                    htmlFilePath = self.reportFile.replace(".md", ".html") if self.reportFile.endswith(".md") else self.reportFile + ".html"
2673                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
2674                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Client's operations", commonCSS=COMMON_CSS, markdown=infoText))
2675
2676                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
2677
2678        return ops, customStat

Returns history operations between two given dates for current accountId. If reportFile string is not empty then also save human-readable report. Shows some statistical data of closed positions.

Parameters
  • start: see docstring in TradeRoutines.GetDatesAsString() method.
  • end: see docstring in TradeRoutines.GetDatesAsString() method.
  • show: if True then also prints all records to the console.
  • showCancelled: if False then remove information about cancelled operations from the deals report.
Returns

original list of dictionaries with history of deals records from API ("operations" key): https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.

def History( self, start: str = None, end: str = None, interval: str = 'hour', onlyMissing: bool = False, csvSep: str = ',', show: bool = False) -> pandas.core.frame.DataFrame:
2680    def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False) -> pd.DataFrame:
2681        """
2682        This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id).
2683
2684        History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`.
2685        Warning! Broker server used ISO UTC time by default.
2686
2687        If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame.
2688        Also, `historyFile` used to update history with `onlyMissing` parameter.
2689
2690        See also: `LoadHistory()` and `ShowHistoryChart()` methods.
2691
2692        :param start: see docstring in `TradeRoutines.GetDatesAsString()` method.
2693        :param end: see docstring in `TradeRoutines.GetDatesAsString()` method.
2694        :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`,
2695                         `"hour"`, `"day"`. Default: `"hour"`.
2696        :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`.
2697                            False by default. Warning! History appends only from last candle to current time
2698                            with always update last candle!
2699        :param csvSep: separator if csv-file is used, `,` by default.
2700        :param show: if `True` then also prints Pandas DataFrame to the console.
2701        :return: Pandas DataFrame with prices history. Headers of columns are defined by default:
2702                 `["date", "time", "open", "high", "low", "close", "volume"]`.
2703        """
2704        strStartDate, strEndDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT)  # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2705        headers = ["date", "time", "open", "high", "low", "close", "volume"]  # sequence and names of column headers
2706        history = None  # empty pandas object for history
2707
2708        if interval not in TKS_CANDLE_INTERVALS.keys():
2709            uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.")
2710            raise Exception("Incorrect value")
2711
2712        if not (self._ticker or self._figi):
2713            uLogger.error("Ticker or FIGI must be defined!")
2714            raise Exception("Ticker or FIGI required")
2715
2716        if self._ticker and not self._figi:
2717            instrumentByTicker = self.SearchByTicker(requestPrice=False)
2718            self._figi = instrumentByTicker["figi"] if instrumentByTicker else ""
2719
2720        if self._figi and not self._ticker:
2721            instrumentByFIGI = self.SearchByFIGI(requestPrice=False)
2722            self._ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else ""
2723
2724        dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from start time string
2725        dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from end time string
2726        if interval.lower() != "day":
2727            dtEnd += timedelta(seconds=1)  # adds 1 sec for requests, because day end returned by `TradeRoutines.GetDatesAsString()` is 23:59:59
2728
2729        delta = dtEnd - dtStart  # current UTC time minus last time in file
2730        deltaMinutes = delta.days * 1440 + delta.seconds // 60  # minutes between start and end dates
2731
2732        # calculate history length in candles:
2733        length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1]
2734        if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0:
2735            length += 1  # to avoid fraction time
2736
2737        # calculate data blocks count:
2738        blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2]
2739
2740        uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end))
2741        uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate))
2742        uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval))
2743        uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2]))
2744        uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self._ticker, self._figi))
2745
2746        tempOld = None  # pandas object for old history, if --only-missing key present
2747        lastTime = None  # datetime object of last old candle in file
2748
2749        if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile):
2750            uLogger.debug("--only-missing key present, add only last missing candles...")
2751            uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile)))
2752
2753            tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers)
2754
2755            tempOld["date"] = pd.to_datetime(tempOld["date"])  # load date "as is"
2756            tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d")  # convert date to string
2757            tempOld["time"] = pd.to_datetime(tempOld["time"])  # load time "as is"
2758            tempOld["time"] = tempOld["time"].dt.strftime("%H:%M")  # convert time to string
2759
2760            # get last datetime object from last string in file or minus 1 delta if file is empty:
2761            if len(tempOld) > 0:
2762                lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2763
2764            else:
2765                lastTime = dtEnd - timedelta(days=1)  # history file is empty, so last date set at -1 day
2766
2767            tempOld = tempOld[:-1]  # always remove last old candle because it may be incompletely at the current time
2768
2769        responseJSONs = []  # raw history blocks of data
2770
2771        blockEnd = dtEnd
2772        for item in range(blocks):
2773            tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2]
2774            blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail)
2775
2776            uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format(
2777                item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2778            ))
2779
2780            if blockStart == blockEnd:
2781                uLogger.debug("Skipped this zero-length block...")
2782
2783            else:
2784                # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles
2785                historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles"
2786                self.body = str({
2787                    "figi": self._figi,
2788                    "from": blockStart.strftime(TKS_DATE_TIME_FORMAT),
2789                    "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2790                    "interval": TKS_CANDLE_INTERVALS[interval][0]
2791                })
2792                responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1)
2793
2794                if "code" in responseJSON.keys():
2795                    uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks))
2796
2797                else:
2798                    if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1:
2799                        responseJSON["candles"] = responseJSON["candles"][:-1]  # removes last candle for "yesterday" request
2800
2801                    responseJSONs = responseJSON["candles"] + responseJSONs  # add more old history behind newest dates
2802
2803            blockEnd = blockStart
2804
2805        printCount = len(responseJSONs)  # candles to show in console
2806        if responseJSONs:
2807            tempHistory = pd.DataFrame(
2808                data={
2809                    "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2810                    "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2811                    "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs],
2812                    "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs],
2813                    "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs],
2814                    "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs],
2815                    "volume": [int(item["volume"]) for item in responseJSONs],
2816                },
2817                index=range(len(responseJSONs)),
2818                columns=["date", "time", "open", "high", "low", "close", "volume"],
2819            )
2820            tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d")
2821            tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M")
2822
2823            # append only newest candles to old history if --only-missing key present:
2824            if onlyMissing and tempOld is not None and lastTime is not None:
2825                index = 0  # find start index in tempHistory data:
2826
2827                for i, item in tempHistory.iterrows():
2828                    curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2829
2830                    if curTime == lastTime:
2831                        uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
2832                        index = i
2833                        printCount = index + 1
2834                        break
2835
2836                history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True)
2837
2838            else:
2839                history = tempHistory  # if no `--only-missing` key then load full data from server
2840
2841            uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False)))
2842
2843        if history is not None and not history.empty:
2844            if show:
2845                uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format(
2846                    strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]),
2847                    pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False),
2848                ))
2849
2850        else:
2851            uLogger.warning("Received an empty candles history!")
2852
2853        if self.historyFile is not None:
2854            if history is not None and not history.empty:
2855                history.to_csv(self.historyFile, sep=csvSep, index=False, header=None)
2856                uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self._ticker, self._figi, interval, os.path.abspath(self.historyFile)))
2857
2858            else:
2859                uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile)))
2860
2861        else:
2862            uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.")
2863
2864        return history

This method returns last history candles of the current instrument defined by ticker or figi (FIGI id).

History returned between two given dates: start and end. Minimum requested date in the past is 1970-01-01. Warning! Broker server used ISO UTC time by default.

If historyFile is not None then method save history to file, otherwise return only Pandas DataFrame. Also, historyFile used to update history with onlyMissing parameter.

See also: LoadHistory() and ShowHistoryChart() methods.

Parameters
  • start: see docstring in TradeRoutines.GetDatesAsString() method.
  • end: see docstring in TradeRoutines.GetDatesAsString() method.
  • interval: this is a candle interval. Current available values are "1min", "5min", "15min", "hour", "day". Default: "hour".
  • onlyMissing: if True then add only last missing candles, do not request all history length from start. False by default. Warning! History appends only from last candle to current time with always update last candle!
  • csvSep: separator if csv-file is used, , by default.
  • show: if True then also prints Pandas DataFrame to the console.
Returns

Pandas DataFrame with prices history. Headers of columns are defined by default: ["date", "time", "open", "high", "low", "close", "volume"].

def LoadHistory(self, filePath: str) -> pandas.core.frame.DataFrame:
2866    def LoadHistory(self, filePath: str) -> pd.DataFrame:
2867        """
2868        Load candles history from csv-file and return Pandas DataFrame object.
2869
2870        See also: `History()` and `ShowHistoryChart()` methods.
2871
2872        :param filePath: path to csv-file to open.
2873        """
2874        loadedHistory = None  # init candles data object
2875
2876        uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...")
2877
2878        if os.path.exists(filePath):
2879            loadedHistory = self.priceModel.LoadFromFile(filePath)  # load data and get chain of candles as Pandas DataFrame
2880
2881            tfStr = self.priceModel.FormattedDelta(
2882                self.priceModel.timeframe,
2883                "{days} days {hours}h {minutes}m {seconds}s",
2884            ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta(
2885                self.priceModel.timeframe,
2886                "{hours}h {minutes}m {seconds}s",
2887            )
2888
2889            if loadedHistory is not None and not loadedHistory.empty:
2890                uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format(
2891                    len(loadedHistory),
2892                    tfStr,
2893                    pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)),
2894                )
2895
2896            else:
2897                uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath)))
2898
2899        else:
2900            uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath))
2901
2902        return loadedHistory

Load candles history from csv-file and return Pandas DataFrame object.

See also: History() and ShowHistoryChart() methods.

Parameters
  • filePath: path to csv-file to open.
def ShowHistoryChart( self, candles: Union[str, pandas.core.frame.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None:
2904    def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None:
2905        """
2906        Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.
2907
2908        Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart.
2909        Default: `index.html` (both for interact and non-interact candlesticks chart).
2910
2911        See also: `History()` and `LoadHistory()` methods.
2912
2913        :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object.
2914        :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart.
2915                         See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters
2916                         If False then chain of candlesticks will render as not interactive Google Candlestick chart.
2917                         See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template
2918        :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to
2919                              html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file.
2920        """
2921        if isinstance(candles, str):
2922            self.priceModel.prices = self.LoadHistory(filePath=candles)  # load candles chain from file
2923            self.priceModel.ticker = os.path.basename(candles)  # use filename as ticker name in PriceGenerator
2924
2925        elif isinstance(candles, pd.DataFrame):
2926            self.priceModel.prices = candles  # set candles chain from variable
2927            self.priceModel.ticker = self._ticker  # use current TKSBrokerAPI ticker as ticker name in PriceGenerator
2928
2929            if "datetime" not in candles.columns:
2930                self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True)  # PriceGenerator uses "datetime" column with date and time
2931
2932        else:
2933            uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!")
2934            raise Exception("Incorrect value")
2935
2936        self.priceModel.horizon = len(self.priceModel.prices)  # use length of candles data as horizon in PriceGenerator
2937
2938        if interact:
2939            uLogger.debug("Rendering interactive candles chart. Wait, please...")
2940
2941            self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2942
2943        else:
2944            uLogger.debug("Rendering non-interactive candles chart. Wait, please...")
2945
2946            self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2947
2948        uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile)))

Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.

Self variable htmlHistoryFile can be use as html-file name to save interaction or non-interaction chart. Default: index.html (both for interact and non-interact candlesticks chart).

See also: History() and LoadHistory() methods.

Parameters
def Trade( self, operation: str, lots: int = 1, tp: float = 0.0, sl: float = 0.0, expDate: str = 'Undefined') -> dict:
2950    def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2951        """
2952        Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response.
2953        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
2954
2955        See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`.
2956
2957        :param operation: string "Buy" or "Sell".
2958        :param lots: volume, integer count of lots >= 1.
2959        :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`.
2960        :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`.
2961        :param expDate: string "Undefined" by default or local date in future,
2962                        it is a string with format `%Y-%m-%d %H:%M:%S`.
2963        :return: JSON with response from broker server.
2964        """
2965        if self.accountId is None or not self.accountId:
2966            uLogger.error("Variable `accountId` must be defined for using this method!")
2967            raise Exception("Account ID required")
2968
2969        if operation is None or not operation or operation not in ("Buy", "Sell"):
2970            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
2971            raise Exception("Incorrect value")
2972
2973        if lots is None or lots < 1:
2974            uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.")
2975            lots = 1
2976
2977        if tp is None or tp < 0:
2978            tp = 0
2979
2980        if sl is None or sl < 0:
2981            sl = 0
2982
2983        if expDate is None or not expDate:
2984            expDate = "Undefined"
2985
2986        if not (self._ticker or self._figi):
2987            uLogger.error("Ticker or FIGI must be defined!")
2988            raise Exception("Ticker or FIGI required")
2989
2990        instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True)
2991        self._ticker = instrument["ticker"]
2992        self._figi = instrument["figi"]
2993
2994        uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self._ticker, self._figi, lots, tp, sl, expDate))
2995
2996        openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
2997        self.body = str({
2998            "figi": self._figi,
2999            "quantity": str(lots),
3000            "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
3001            "accountId": str(self.accountId),
3002            "orderType": "ORDER_TYPE_MARKET",  # see: TKS_ORDER_TYPES
3003        })
3004        response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0)
3005
3006        if "orderId" in response.keys():
3007            uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format(
3008                operation, response["orderId"],
3009                self._ticker, self._figi, lots,
3010                NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"],
3011                NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"],
3012                NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"],
3013            ))
3014
3015            if tp > 0:
3016                self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate)
3017
3018            if sl > 0:
3019                self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate)
3020
3021        else:
3022            uLogger.warning("Not `oK` status received! Market order not executed. See full debug log and try again open order later.")
3023
3024        return response

Universal method to create market order and make deal at the current price for current accountId. Returns JSON data with response. If tp or sl > 0, then in additional will open stop-orders with "TP" and "SL" flags for stopType parameter.

See also: Order() docstring. More simple methods than Trade() are Buy() and Sell().

Parameters
  • operation: string "Buy" or "Sell".
  • lots: volume, integer count of lots >= 1.
  • tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter targetPrice in self.Order().
  • sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter targetPrice in self.Order().
  • expDate: string "Undefined" by default or local date in future, it is a string with format %Y-%m-%d %H:%M:%S.
Returns

JSON with response from broker server.

def Buy( self, lots: int = 1, tp: float = 0.0, sl: float = 0.0, expDate: str = 'Undefined') -> dict:
3026    def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
3027        """
3028        More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response.
3029        If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter.
3030
3031        See also: `Order()` and `Trade()` docstrings.
3032
3033        :param lots: volume, integer count of lots >= 1.
3034        :param tp: float > 0, take profit price of stop-order.
3035        :param sl: float > 0, stop loss price of stop-order.
3036        :param expDate: it's a local date in future.
3037                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3038        :return: JSON with response from broker server.
3039        """
3040        return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate)

More simple method than Trade(). Create Buy market order and make deal at the current price. Returns JSON data with response. If tp or sl > 0, then in additional will opens stop-orders with "TP" and "SL" flags for stopType parameter.

See also: Order() and Trade() docstrings.

Parameters
  • lots: volume, integer count of lots >= 1.
  • tp: float > 0, take profit price of stop-order.
  • sl: float > 0, stop loss price of stop-order.
  • expDate: it's a local date in future. String has a format like this: %Y-%m-%d %H:%M:%S.
Returns

JSON with response from broker server.

def Sell( self, lots: int = 1, tp: float = 0.0, sl: float = 0.0, expDate: str = 'Undefined') -> dict:
3042    def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
3043        """
3044        More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response.
3045        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
3046
3047        See also: `Order()` and `Trade()` docstrings.
3048
3049        :param lots: volume, integer count of lots >= 1.
3050        :param tp: float > 0, take profit price of stop-order.
3051        :param sl: float > 0, stop loss price of stop-order.
3052        :param expDate: it's a local date in the future.
3053                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3054        :return: JSON with response from broker server.
3055        """
3056        return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate)

More simple method than Trade(). Create Sell market order and make deal at the current price. Returns JSON data with response. If tp or sl > 0, then in additional will open stop-orders with "TP" and "SL" flags for stopType parameter.

See also: Order() and Trade() docstrings.

Parameters
  • lots: volume, integer count of lots >= 1.
  • tp: float > 0, take profit price of stop-order.
  • sl: float > 0, stop loss price of stop-order.
  • expDate: it's a local date in the future. String has a format like this: %Y-%m-%d %H:%M:%S.
Returns

JSON with response from broker server.

def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None:
3058    def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None:
3059        """
3060        Close position of given instruments.
3061
3062        :param instruments: list of instruments defined by tickers or FIGIs that must be closed.
3063        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
3064                         This avoids unnecessary downloading data from the server.
3065        """
3066        if instruments is None or not instruments:
3067            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
3068            raise Exception("Ticker or FIGI required")
3069
3070        if isinstance(instruments, str):
3071            instruments = [instruments]
3072
3073        uniqueInstruments = self.GetUniqueFIGIs(instruments)
3074        if uniqueInstruments:
3075            if portfolio is None or not portfolio:
3076                portfolio = self.Overview(show=False)
3077
3078            allOpened = [item["figi"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]]
3079            uLogger.debug("All opened instruments by it's FIGI: {}".format(", ".join(allOpened)))
3080
3081            for self._figi in uniqueInstruments:
3082                if self._figi not in allOpened:
3083                    uLogger.warning("Instrument with FIGI [{}] not in open positions list!".format(self._figi))
3084                    continue
3085
3086                # search open trade info about instrument by ticker:
3087                instrument = {}
3088                for iType in TKS_INSTRUMENTS:
3089                    if instrument:
3090                        break
3091
3092                    for item in portfolio["stat"][iType]:
3093                        if item["figi"] == self._figi:
3094                            instrument = item
3095                            break
3096
3097                if instrument:
3098                    self._ticker = instrument["ticker"]
3099                    self._figi = instrument["figi"]
3100
3101                    uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format(
3102                        self._ticker,
3103                        self._figi,
3104                        int(instrument["volume"]),
3105                        ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "",
3106                    ))
3107
3108                    tradeLots = abs(instrument["lots"]) - instrument["blocked"]  # available volumes in lots for close operation
3109
3110                    if tradeLots > 0:
3111                        if instrument["blocked"] > 0:
3112                            uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format(
3113                                instrument["blocked"],
3114                                self._ticker,
3115                                tradeLots,
3116                            ))
3117
3118                        # if direction is "Long" then we need sell, if direction is "Short" then we need buy:
3119                        self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots)
3120
3121                    else:
3122                        uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self._ticker))

Close position of given instruments.

Parameters
  • instruments: list of instruments defined by tickers or FIGIs that must be closed.
  • portfolio: pre-received dictionary with open trades, returned by Overview() method. This avoids unnecessary downloading data from the server.
def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None:
3124    def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None:
3125        """
3126        Close all positions of given instruments with defined type.
3127
3128        :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
3129        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
3130                         This avoids unnecessary downloading data from the server.
3131        """
3132        if iType not in TKS_INSTRUMENTS:
3133            uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType))
3134
3135        else:
3136            if portfolio is None or not portfolio:
3137                portfolio = self.Overview(show=False)
3138
3139            tickers = [item["ticker"] for item in portfolio["stat"][iType]]
3140            uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers))
3141
3142            if tickers and portfolio:
3143                self.CloseTrades(tickers, portfolio)
3144
3145            else:
3146                uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType))

Close all positions of given instruments with defined type.

Parameters
  • iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
  • portfolio: pre-received dictionary with open trades, returned by Overview() method. This avoids unnecessary downloading data from the server.
def Order( self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0.0, stopType: str = 'Limit', expDate: str = 'Undefined') -> dict:
3148    def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3149        """
3150        Universal method to create market or limit orders with all available parameters for current `accountId`.
3151        See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`.
3152
3153        If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above
3154        current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.
3155
3156        Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell"
3157        then broker immediately open market order as you can do simple --buy or --sell operations!
3158
3159        If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell".
3160        When current price will go up or down to target price value then broker opens a limit order.
3161        Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.
3162
3163        Only one attempt and no retry for opens order. If network issue occurred you can create new request.
3164
3165        :param operation: string "Buy" or "Sell".
3166        :param orderType: string "Limit" or "Stop".
3167        :param lots: volume, integer count of lots >= 1.
3168        :param targetPrice: target price > 0. This is open trade price for limit order.
3169        :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice.
3170                           Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
3171        :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types
3172                         "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3173                         Stop loss order always executed by market price.
3174        :param expDate: string "Undefined" by default or local date in future.
3175                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3176                        This date is converting to UTC format for server. This parameter only makes sense for stop-order.
3177                        A limit order has no expiration date, it lasts until the end of the trading day.
3178        :return: JSON with response from broker server.
3179        """
3180        if self.accountId is None or not self.accountId:
3181            uLogger.error("Variable `accountId` must be defined for using this method!")
3182            raise Exception("Account ID required")
3183
3184        if operation is None or not operation or operation not in ("Buy", "Sell"):
3185            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
3186            raise Exception("Incorrect value")
3187
3188        if orderType is None or not orderType or orderType not in ("Limit", "Stop"):
3189            uLogger.error("You must define order type only one of them: `Limit` or `Stop`!")
3190            raise Exception("Incorrect value")
3191
3192        if lots is None or lots < 1:
3193            uLogger.error("You must define trade volume > 0: integer count of lots!")
3194            raise Exception("Incorrect value")
3195
3196        if targetPrice is None or targetPrice <= 0:
3197            uLogger.error("Target price for limit-order must be greater than 0!")
3198            raise Exception("Incorrect value")
3199
3200        if limitPrice is None or limitPrice <= 0:
3201            limitPrice = targetPrice
3202
3203        if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"):
3204            stopType = "Limit"
3205
3206        if expDate is None or not expDate:
3207            expDate = "Undefined"
3208
3209        if not (self._ticker or self._figi):
3210            uLogger.error("Tocker or FIGI must be defined!")
3211            raise Exception("Ticker or FIGI required")
3212
3213        response = {}
3214        instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True)
3215        self._ticker = instrument["ticker"]
3216        self._figi = instrument["figi"]
3217
3218        if orderType == "Limit":
3219            uLogger.debug(
3220                "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format(
3221                    self._ticker, self._figi,
3222                    operation, lots, targetPrice, instrument["currency"],
3223                ))
3224
3225            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
3226            self.body = str({
3227                "figi": self._figi,
3228                "quantity": str(lots),
3229                "price": FloatToNano(targetPrice),
3230                "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
3231                "accountId": str(self.accountId),
3232                "orderType": "ORDER_TYPE_LIMIT",  # see: TKS_ORDER_TYPES
3233            })
3234            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0)
3235
3236            if "orderId" in response.keys():
3237                uLogger.info(
3238                    "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{} {}]".format(
3239                        response["orderId"], self._ticker, self._figi, operation, lots,
3240                        "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"],
3241                    ))
3242
3243                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3244                    if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]:
3245                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format(
3246                            targetPrice, instrument["currency"],
3247                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3248                        ))
3249
3250                    if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]:
3251                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format(
3252                            targetPrice, instrument["currency"],
3253                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3254                        ))
3255
3256            else:
3257                uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log and try again open order later.")
3258
3259        if orderType == "Stop":
3260            uLogger.debug(
3261                "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format(
3262                    self._ticker, self._figi,
3263                    operation, lots,
3264                    targetPrice, instrument["currency"],
3265                    limitPrice, instrument["currency"],
3266                    stopType, expDate,
3267                ))
3268
3269            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder"
3270            expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT)
3271            stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT"
3272
3273            body = {
3274                "figi": self._figi,
3275                "quantity": str(lots),
3276                "price": FloatToNano(limitPrice),
3277                "stopPrice": FloatToNano(targetPrice),
3278                "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL",  # see: TKS_STOP_ORDER_DIRECTIONS
3279                "accountId": str(self.accountId),
3280                "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL",  # see: TKS_STOP_ORDER_EXPIRATION_TYPES
3281                "stopOrderType": stopOrderType,  # see: TKS_STOP_ORDER_TYPES
3282            }
3283
3284            if expDateUTC:
3285                body["expireDate"] = expDateUTC
3286
3287            self.body = str(body)
3288            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0)
3289
3290            if "stopOrderId" in response.keys():
3291                uLogger.info(
3292                    "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{} {}], limit price [{} {}], stop-order type [{}] and expiration date [{} UTC]".format(
3293                        response["stopOrderId"], self._ticker, self._figi, operation, lots,
3294                        "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"],
3295                        "{:.4f}".format(limitPrice).rstrip("0").rstrip("."), instrument["currency"],
3296                        TKS_STOP_ORDER_TYPES[stopOrderType],
3297                        datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"],
3298                    ))
3299
3300                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3301                    if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3302                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3303                            targetPrice, instrument["currency"],
3304                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3305                        ))
3306
3307                    if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3308                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3309                            targetPrice, instrument["currency"],
3310                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3311                        ))
3312
3313            else:
3314                uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log and try again open order later.")
3315
3316        return response

Universal method to create market or limit orders with all available parameters for current accountId. See more simple methods: BuyLimit(), BuyStop(), SellLimit(), SellStop().

If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.

Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" then broker immediately open market order as you can do simple --buy or --sell operations!

If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". When current price will go up or down to target price value then broker opens a limit order. Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.

Only one attempt and no retry for opens order. If network issue occurred you can create new request.

Parameters
  • operation: string "Buy" or "Sell".
  • orderType: string "Limit" or "Stop".
  • lots: volume, integer count of lots >= 1.
  • targetPrice: target price > 0. This is open trade price for limit order.
  • limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
  • stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. Stop loss order always executed by market price.
  • expDate: string "Undefined" by default or local date in future. String has a format like this: %Y-%m-%d %H:%M:%S. This date is converting to UTC format for server. This parameter only makes sense for stop-order. A limit order has no expiration date, it lasts until the end of the trading day.
Returns

JSON with response from broker server.

def BuyLimit(self, lots: int, targetPrice: float) -> dict:
3318    def BuyLimit(self, lots: int, targetPrice: float) -> dict:
3319        """
3320        Create pending `Buy` limit-order (below current price). You must specify only 2 parameters:
3321        `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then
3322        broker immediately open `Buy` market order, such as if you do simple `--buy` operation!
3323        See also: `Order()` docstring.
3324
3325        :param lots: volume, integer count of lots >= 1.
3326        :param targetPrice: target price > 0. This is open trade price for limit order.
3327        :return: JSON with response from broker server.
3328        """
3329        return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice)

Create pending Buy limit-order (below current price). You must specify only 2 parameters: lots and target price to open buy limit-order. If you try to create buy limit-order above current price then broker immediately open Buy market order, such as if you do simple --buy operation! See also: Order() docstring.

Parameters
  • lots: volume, integer count of lots >= 1.
  • targetPrice: target price > 0. This is open trade price for limit order.
Returns

JSON with response from broker server.

def BuyStop( self, lots: int, targetPrice: float, limitPrice: float = 0.0, stopType: str = 'Limit', expDate: str = 'Undefined') -> dict:
3331    def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3332        """
3333        Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order.
3334        In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3335        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3336        target price value then broker opens a limit order. See also: `Order()` docstring.
3337
3338        :param lots: volume, integer count of lots >= 1.
3339        :param targetPrice: target price > 0. This is trigger price for buy stop-order.
3340        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3341                           with price equal to limitPrice, when current price goes to target price of buy stop-order.
3342        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3343                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3344        :param expDate: string "Undefined" by default or local date in future.
3345                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3346                        This date is converting to UTC format for server.
3347        :return: JSON with response from broker server.
3348        """
3349        return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)

Create Buy stop-order. You must specify at least 2 parameters: lots target price to open buy stop-order. In additional you can specify 3 parameters for buy stop-order: limit price >=0, stop type = Limit|SL|TP, expiration date = Undefined|%%Y-%%m-%%d %%H:%%M:%%S. When current price will go up or down to target price value then broker opens a limit order. See also: Order() docstring.

Parameters
  • lots: volume, integer count of lots >= 1.
  • targetPrice: target price > 0. This is trigger price for buy stop-order.
  • limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of buy stop-order.
  • stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
  • expDate: string "Undefined" by default or local date in future. String has a format like this: %Y-%m-%d %H:%M:%S. This date is converting to UTC format for server.
Returns

JSON with response from broker server.

def SellLimit(self, lots: int, targetPrice: float) -> dict:
3351    def SellLimit(self, lots: int, targetPrice: float) -> dict:
3352        """
3353        Create pending `Sell` limit-order (above current price). You must specify only 2 parameters:
3354        `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then
3355        broker immediately open `Sell` market order, such as if you do simple `--sell` operation!
3356        See also: `Order()` docstring.
3357
3358        :param lots: volume, integer count of lots >= 1.
3359        :param targetPrice: target price > 0. This is open trade price for limit order.
3360        :return: JSON with response from broker server.
3361        """
3362        return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice)

Create pending Sell limit-order (above current price). You must specify only 2 parameters: lots and target price to open sell limit-order. If you try to create sell limit-order below current price then broker immediately open Sell market order, such as if you do simple --sell operation! See also: Order() docstring.

Parameters
  • lots: volume, integer count of lots >= 1.
  • targetPrice: target price > 0. This is open trade price for limit order.
Returns

JSON with response from broker server.

def SellStop( self, lots: int, targetPrice: float, limitPrice: float = 0.0, stopType: str = 'Limit', expDate: str = 'Undefined') -> dict:
3364    def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3365        """
3366        Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order.
3367        In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3368        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3369        target price value then broker opens a limit order. See also: `Order()` docstring.
3370
3371        :param lots: volume, integer count of lots >= 1.
3372        :param targetPrice: target price > 0. This is trigger price for sell stop-order.
3373        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3374                           with price equal to limitPrice, when current price goes to target price of sell stop-order.
3375        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3376                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3377        :param expDate: string "Undefined" by default or local date in future.
3378                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3379                        This date is converting to UTC format for server.
3380        :return: JSON with response from broker server.
3381        """
3382        return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)

Create Sell stop-order. You must specify at least 2 parameters: lots target price to open sell stop-order. In additional you can specify 3 parameters for sell stop-order: limit price >=0, stop type = Limit|SL|TP, expiration date = Undefined|%%Y-%%m-%%d %%H:%%M:%%S. When current price will go up or down to target price value then broker opens a limit order. See also: Order() docstring.

Parameters
  • lots: volume, integer count of lots >= 1.
  • targetPrice: target price > 0. This is trigger price for sell stop-order.
  • limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of sell stop-order.
  • stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
  • expDate: string "Undefined" by default or local date in future. String has a format like this: %Y-%m-%d %H:%M:%S. This date is converting to UTC format for server.
Returns

JSON with response from broker server.

def CloseOrders( self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None:
3384    def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None:
3385        """
3386        Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`.
3387
3388        :param orderIDs: list of integers with `orderId` or `stopOrderId`.
3389        :param allOrdersIDs: pre-received lists of all active pending limit orders.
3390                             This avoids unnecessary downloading data from the server.
3391        :param allStopOrdersIDs: pre-received lists of all active stop orders.
3392        """
3393        if self.accountId is None or not self.accountId:
3394            uLogger.error("Variable `accountId` must be defined for using this method!")
3395            raise Exception("Account ID required")
3396
3397        if orderIDs:
3398            if allOrdersIDs is None:
3399                rawOrders = self.RequestPendingOrders()
3400                allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending limit orders ID
3401
3402            if allStopOrdersIDs is None:
3403                rawStopOrders = self.RequestStopOrders()
3404                allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3405
3406            for orderID in orderIDs:
3407                idInPendingOrders = orderID in allOrdersIDs
3408                idInStopOrders = orderID in allStopOrdersIDs
3409
3410                if not (idInPendingOrders or idInStopOrders):
3411                    uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID))
3412                    continue
3413
3414                else:
3415                    if idInPendingOrders:
3416                        uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID))
3417
3418                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder
3419                        self.body = str({"accountId": self.accountId, "orderId": orderID})
3420                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder"
3421                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3422
3423                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3424                            if self.moreDebug:
3425                                uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3426
3427                            uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID))
3428
3429                        else:
3430                            uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID))
3431
3432                    elif idInStopOrders:
3433                        uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID))
3434
3435                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder
3436                        self.body = str({"accountId": self.accountId, "stopOrderId": orderID})
3437                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder"
3438                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3439
3440                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3441                            if self.moreDebug:
3442                                uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3443
3444                            uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID))
3445
3446                        else:
3447                            uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID))
3448
3449                    else:
3450                        continue

Cancel order or list of orders by its orderId or stopOrderId for current accountId.

Parameters
  • orderIDs: list of integers with orderId or stopOrderId.
  • allOrdersIDs: pre-received lists of all active pending limit orders. This avoids unnecessary downloading data from the server.
  • allStopOrdersIDs: pre-received lists of all active stop orders.
def CloseAllOrders(self) -> None:
3452    def CloseAllOrders(self) -> None:
3453        """
3454        Gets a list of open pending and stop orders and cancel it all.
3455        """
3456        rawOrders = self.RequestPendingOrders()
3457        allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending limit orders ID
3458        lenOrders = len(allOrdersIDs)
3459
3460        rawStopOrders = self.RequestStopOrders()
3461        allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3462        lenSOrders = len(allStopOrdersIDs)
3463
3464        if lenOrders > 0 or lenSOrders > 0:
3465            uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders))
3466
3467            self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs)
3468
3469        else:
3470            uLogger.info("Orders not found, nothing to cancel.")

Gets a list of open pending and stop orders and cancel it all.

def CloseAll(self, *args) -> None:
3472    def CloseAll(self, *args) -> None:
3473        """
3474        Close all available (not blocked) opened trades and orders.
3475
3476        Also, you can select one or more keywords case-insensitive:
3477        `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type.
3478
3479        Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods.
3480        """
3481        overview = self.Overview(show=False)  # get all open trades info
3482
3483        if len(args) == 0:
3484            uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...")
3485            self.CloseAllOrders()  # close all pending and stop orders
3486
3487            for iType in TKS_INSTRUMENTS:
3488                if iType != "Currencies":
3489                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies
3490
3491        else:
3492            uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args)))
3493            lowerArgs = [x.lower() for x in args]
3494
3495            if "orders" in lowerArgs:
3496                self.CloseAllOrders()  # close all pending and stop orders
3497
3498            for iType in TKS_INSTRUMENTS:
3499                if iType.lower() in lowerArgs and iType != "Currencies":
3500                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies

Close all available (not blocked) opened trades and orders.

Also, you can select one or more keywords case-insensitive: orders, shares, bonds, etfs and futures from TKS_INSTRUMENTS enum to specify trades type.

Currency positions you must close manually using buy or sell operations, CloseTrades() or CloseAllTrades() methods.

def CloseAllByTicker(self, instrument: str) -> None:
3502    def CloseAllByTicker(self, instrument: str) -> None:
3503        """
3504        Close all available (not blocked) opened trades and orders for one instrument defined by its ticker.
3505
3506        This method searches opened trade and orders of instrument throw all portfolio and then use
3507        `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument.
3508
3509        See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`.
3510
3511        :param instrument: string with ticker.
3512        """
3513        if instrument is None or not instrument:
3514            uLogger.error("Ticker name must be defined for using this method!")
3515            raise Exception("Ticker required")
3516
3517        overview = self.Overview(show=False)  # get user portfolio with all open trades info
3518
3519        self._ticker = instrument  # try to set instrument as ticker
3520        self._figi = ""
3521
3522        limitAll = [item["orderID"] for item in overview["stat"]["orders"]]  # list of all pending limit order IDs
3523        stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]]  # list of all stop order IDs
3524
3525        if limitAll and self.IsInLimitOrders(portfolio=overview):
3526            uLogger.debug("Closing all opened pending limit orders for the instrument with ticker [{}]. Wait, please...")
3527            self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3528
3529        if stopAll and self.IsInStopOrders(portfolio=overview):
3530            uLogger.debug("Closing all opened stop orders for the instrument with ticker [{}]. Wait, please...")
3531            self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3532
3533        if self.IsInPortfolio(portfolio=overview):
3534            uLogger.debug("Closing all available (not blocked) opened trade for the instrument with ticker [{}]. Wait, please...")
3535            self.CloseTrades(instruments=[instrument], portfolio=overview)

Close all available (not blocked) opened trades and orders for one instrument defined by its ticker.

This method searches opened trade and orders of instrument throw all portfolio and then use CloseTrades() and CloseOrders() methods to close trade and cancel all orders for that instrument.

See also: IsInLimitOrders(), GetLimitOrderIDs(), IsInStopOrders(), GetStopOrderIDs(), CloseTrades() and CloseOrders().

Parameters
  • instrument: string with ticker.
def CloseAllByFIGI(self, instrument: str) -> None:
3537    def CloseAllByFIGI(self, instrument: str) -> None:
3538        """
3539        Close all available (not blocked) opened trades and orders for one instrument defined by its FIGI id.
3540
3541        This method searches opened trade and orders of instrument throw all portfolio and then use
3542        `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument.
3543
3544        See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`.
3545
3546        :param instrument: string with FIGI id.
3547        """
3548        if instrument is None or not instrument:
3549            uLogger.error("FIGI id must be defined for using this method!")
3550            raise Exception("FIGI required")
3551
3552        overview = self.Overview(show=False)  # get user portfolio with all open trades info
3553
3554        self._ticker = ""
3555        self._figi = instrument  # try to set instrument as FIGI id
3556
3557        limitAll = [item["orderID"] for item in overview["stat"]["orders"]]  # list of all pending limit order IDs
3558        stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]]  # list of all stop order IDs
3559
3560        if limitAll and self.IsInLimitOrders(portfolio=overview):
3561            uLogger.debug("Closing all opened pending limit orders for the instrument with FIGI [{}]. Wait, please...")
3562            self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3563
3564        if stopAll and self.IsInStopOrders(portfolio=overview):
3565            uLogger.debug("Closing all opened stop orders for the instrument with FIGI [{}]. Wait, please...")
3566            self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3567
3568        if self.IsInPortfolio(portfolio=overview):
3569            uLogger.debug("Closing all available (not blocked) opened trade for the instrument with FIGI [{}]. Wait, please...")
3570            self.CloseTrades(instruments=[instrument], portfolio=overview)

Close all available (not blocked) opened trades and orders for one instrument defined by its FIGI id.

This method searches opened trade and orders of instrument throw all portfolio and then use CloseTrades() and CloseOrders() methods to close trade and cancel all orders for that instrument.

See also: IsInLimitOrders(), GetLimitOrderIDs(), IsInStopOrders(), GetStopOrderIDs(), CloseTrades() and CloseOrders().

Parameters
  • instrument: string with FIGI id.
@staticmethod
def ParseOrderParameters(operation, **inputParameters):
3572    @staticmethod
3573    def ParseOrderParameters(operation, **inputParameters):
3574        """
3575        Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.
3576
3577        :param operation: string "Buy" or "Sell".
3578        :param inputParameters: this is dict of strings that looks like this
3579               `{"lots": "L_int,...", "prices": "P_float,..."}` where
3580               "lots" key: one or more lot values (integer numbers) to open with every limit-order
3581               "prices" key: one or more prices to open limit-orders
3582               Counts of values in lots and prices lists must be equals!
3583        :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]`
3584        """
3585        # TODO: update order grid work with api v2
3586        pass
3587        # uLogger.debug("Input parameters: {}".format(inputParameters))
3588        #
3589        # if operation is None or not operation or operation not in ("Buy", "Sell"):
3590        #     uLogger.error("You must define operation type: 'Buy' or 'Sell'!")
3591        #     raise Exception("Incorrect value")
3592        #
3593        # if "l" in inputParameters.keys():
3594        #     inputParameters["lots"] = inputParameters.pop("l")
3595        #
3596        # if "p" in inputParameters.keys():
3597        #     inputParameters["prices"] = inputParameters.pop("p")
3598        #
3599        # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys():
3600        #     uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!")
3601        #     raise Exception("Incorrect value")
3602        #
3603        # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")]
3604        # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")]
3605        #
3606        # if len(lots) != len(prices):
3607        #     uLogger.error("'lots' and 'prices' lists must have equal length of values!")
3608        #     raise Exception("Incorrect value")
3609        #
3610        # uLogger.debug("Extracted parameters for orders:")
3611        # uLogger.debug("lots = {}".format(lots))
3612        # uLogger.debug("prices = {}".format(prices))
3613        #
3614        # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...]
3615        # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))]
3616        # uLogger.debug("Order parameters: {}".format(result))
3617        #
3618        # return result

Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.

Parameters
  • operation: string "Buy" or "Sell".
  • inputParameters: this is dict of strings that looks like this {"lots": "L_int,...", "prices": "P_float,..."} where "lots" key: one or more lot values (integer numbers) to open with every limit-order "prices" key: one or more prices to open limit-orders Counts of values in lots and prices lists must be equals!
Returns

list of dictionaries with all lots and prices to open orders that looks like this [{"lot": lots_1, "price": price_1}, {...}, ...]

def IsInPortfolio(self, portfolio: dict = None) -> bool:
3620    def IsInPortfolio(self, portfolio: dict = None) -> bool:
3621        """
3622        Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`.
3623
3624        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3625        :return: `True` if portfolio contains open position with given instrument, `False` otherwise.
3626        """
3627        result = False
3628        msg = "Instrument not defined!"
3629
3630        if portfolio is None or not portfolio:
3631            portfolio = self.Overview(show=False)
3632
3633        if self._ticker:
3634            uLogger.debug("Searching instrument with ticker [{}] throw opened positions list...".format(self._ticker))
3635            msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker)
3636
3637            for iType in TKS_INSTRUMENTS:
3638                for instrument in portfolio["stat"][iType]:
3639                    if instrument["ticker"] == self._ticker:
3640                        result = True
3641                        msg = "Instrument with ticker [{}] is present in open positions".format(self._ticker)
3642                        break
3643
3644        elif self._figi:
3645            uLogger.debug("Searching instrument with FIGI [{}] throw opened positions list...".format(self._figi))
3646            msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi)
3647
3648            for iType in TKS_INSTRUMENTS:
3649                for instrument in portfolio["stat"][iType]:
3650                    if instrument["figi"] == self._figi:
3651                        result = True
3652                        msg = "Instrument with FIGI [{}] is present in open positions".format(self._figi)
3653                        break
3654
3655        else:
3656            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3657
3658        uLogger.debug(msg)
3659
3660        return result

Checks if instrument is in the user's portfolio. Instrument must be defined by ticker (highly priority) or figi.

Parameters
  • portfolio: dict with user's portfolio data. If None, then requests portfolio from Overview() method.
Returns

True if portfolio contains open position with given instrument, False otherwise.

def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict:
3662    def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict:
3663        """
3664        Returns instrument from the user's portfolio if it presents there.
3665        Instrument must be defined by `ticker` (highly priority) or `figi`.
3666
3667        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3668        :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise.
3669        """
3670        result = None
3671        msg = "Instrument not defined!"
3672
3673        if portfolio is None or not portfolio:
3674            portfolio = self.Overview(show=False)
3675
3676        if self._ticker:
3677            uLogger.debug("Searching instrument with ticker [{}] in opened positions...".format(self._ticker))
3678            msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker)
3679
3680            for iType in TKS_INSTRUMENTS:
3681                for instrument in portfolio["stat"][iType]:
3682                    if instrument["ticker"] == self._ticker:
3683                        result = instrument
3684                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self._ticker, instrument["figi"])
3685                        break
3686
3687        elif self._figi:
3688            uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self._figi))
3689            msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi)
3690
3691            for iType in TKS_INSTRUMENTS:
3692                for instrument in portfolio["stat"][iType]:
3693                    if instrument["figi"] == self._figi:
3694                        result = instrument
3695                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self._figi)
3696                        break
3697
3698        else:
3699            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3700
3701        uLogger.debug(msg)
3702
3703        return result

Returns instrument from the user's portfolio if it presents there. Instrument must be defined by ticker (highly priority) or figi.

Parameters
  • portfolio: dict with user's portfolio data. If None, then requests portfolio from Overview() method.
Returns

dict with instrument if portfolio contains open position with this instrument, None otherwise.

def IsInLimitOrders(self, portfolio: dict = None) -> bool:
3705    def IsInLimitOrders(self, portfolio: dict = None) -> bool:
3706        """
3707        Checks if instrument is in the limit orders list. Instrument must be defined by `ticker` (highly priority) or `figi`.
3708
3709        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3710
3711        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3712        :return: `True` if limit orders list contains some limit orders for the instrument, `False` otherwise.
3713        """
3714        result = False
3715        msg = "Instrument not defined!"
3716
3717        if portfolio is None or not portfolio:
3718            portfolio = self.Overview(show=False)
3719
3720        if self._ticker:
3721            uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker))
3722            msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker)
3723
3724            for instrument in portfolio["stat"]["orders"]:
3725                if instrument["ticker"] == self._ticker:
3726                    result = True
3727                    msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker)
3728                    break
3729
3730        elif self._figi:
3731            uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi))
3732            msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi)
3733
3734            for instrument in portfolio["stat"]["orders"]:
3735                if instrument["figi"] == self._figi:
3736                    result = True
3737                    msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi)
3738                    break
3739
3740        else:
3741            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3742
3743        uLogger.debug(msg)
3744
3745        return result

Checks if instrument is in the limit orders list. Instrument must be defined by ticker (highly priority) or figi.

See also: CloseAllByTicker() and CloseAllByFIGI().

Parameters
  • portfolio: dict with user's portfolio data. If None, then requests portfolio from Overview() method.
Returns

True if limit orders list contains some limit orders for the instrument, False otherwise.

def GetLimitOrderIDs(self, portfolio: dict = None) -> list[str]:
3747    def GetLimitOrderIDs(self, portfolio: dict = None) -> list[str]:
3748        """
3749        Returns list with all `orderID`s of opened pending limit orders for the instrument.
3750        Instrument must be defined by `ticker` (highly priority) or `figi`.
3751
3752        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3753
3754        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3755        :return: list with `orderID`s of limit orders.
3756        """
3757        result = []
3758        msg = "Instrument not defined!"
3759
3760        if portfolio is None or not portfolio:
3761            portfolio = self.Overview(show=False)
3762
3763        if self._ticker:
3764            uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker))
3765            msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker)
3766
3767            for instrument in portfolio["stat"]["orders"]:
3768                if instrument["ticker"] == self._ticker:
3769                    result.append(instrument["orderID"])
3770
3771            if result:
3772                msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker)
3773
3774        elif self._figi:
3775            uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi))
3776            msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi)
3777
3778            for instrument in portfolio["stat"]["orders"]:
3779                if instrument["figi"] == self._figi:
3780                    result.append(instrument["orderID"])
3781
3782            if result:
3783                msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi)
3784
3785        else:
3786            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3787
3788        uLogger.debug(msg)
3789
3790        return result

Returns list with all orderIDs of opened pending limit orders for the instrument. Instrument must be defined by ticker (highly priority) or figi.

See also: CloseAllByTicker() and CloseAllByFIGI().

Parameters
  • portfolio: dict with user's portfolio data. If None, then requests portfolio from Overview() method.
Returns

list with orderIDs of limit orders.

def IsInStopOrders(self, portfolio: dict = None) -> bool:
3792    def IsInStopOrders(self, portfolio: dict = None) -> bool:
3793        """
3794        Checks if instrument is in the stop orders list. Instrument must be defined by `ticker` (highly priority) or `figi`.
3795
3796        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3797
3798        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3799        :return: `True` if stop orders list contains some stop orders for the instrument, `False` otherwise.
3800        """
3801        result = False
3802        msg = "Instrument not defined!"
3803
3804        if portfolio is None or not portfolio:
3805            portfolio = self.Overview(show=False)
3806
3807        if self._ticker:
3808            uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker))
3809            msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker)
3810
3811            for instrument in portfolio["stat"]["stopOrders"]:
3812                if instrument["ticker"] == self._ticker:
3813                    result = True
3814                    msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker)
3815                    break
3816
3817        elif self._figi:
3818            uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi))
3819            msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi)
3820
3821            for instrument in portfolio["stat"]["stopOrders"]:
3822                if instrument["figi"] == self._figi:
3823                    result = True
3824                    msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi)
3825                    break
3826
3827        else:
3828            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3829
3830        uLogger.debug(msg)
3831
3832        return result

Checks if instrument is in the stop orders list. Instrument must be defined by ticker (highly priority) or figi.

See also: CloseAllByTicker() and CloseAllByFIGI().

Parameters
  • portfolio: dict with user's portfolio data. If None, then requests portfolio from Overview() method.
Returns

True if stop orders list contains some stop orders for the instrument, False otherwise.

def GetStopOrderIDs(self, portfolio: dict = None) -> list[str]:
3834    def GetStopOrderIDs(self, portfolio: dict = None) -> list[str]:
3835        """
3836        Returns list with all `orderID`s of opened stop orders for the instrument.
3837        Instrument must be defined by `ticker` (highly priority) or `figi`.
3838
3839        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3840
3841        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3842        :return: list with `orderID`s of stop orders.
3843        """
3844        result = []
3845        msg = "Instrument not defined!"
3846
3847        if portfolio is None or not portfolio:
3848            portfolio = self.Overview(show=False)
3849
3850        if self._ticker:
3851            uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker))
3852            msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker)
3853
3854            for instrument in portfolio["stat"]["stopOrders"]:
3855                if instrument["ticker"] == self._ticker:
3856                    result.append(instrument["orderID"])
3857
3858            if result:
3859                msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker)
3860
3861        elif self._figi:
3862            uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi))
3863            msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi)
3864
3865            for instrument in portfolio["stat"]["stopOrders"]:
3866                if instrument["figi"] == self._figi:
3867                    result.append(instrument["orderID"])
3868
3869            if result:
3870                msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi)
3871
3872        else:
3873            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3874
3875        uLogger.debug(msg)
3876
3877        return result

Returns list with all orderIDs of opened stop orders for the instrument. Instrument must be defined by ticker (highly priority) or figi.

See also: CloseAllByTicker() and CloseAllByFIGI().

Parameters
  • portfolio: dict with user's portfolio data. If None, then requests portfolio from Overview() method.
Returns

list with orderIDs of stop orders.

def RequestLimits(self) -> dict:
3879    def RequestLimits(self) -> dict:
3880        """
3881        Method for obtaining the available funds for withdrawal for current `accountId`.
3882
3883        See also:
3884        - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits
3885        - `OverviewLimits()` method
3886
3887        :return: dict with raw data from server that contains free funds for withdrawal. Example of dict:
3888                 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`.
3889                 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency
3890                 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures.
3891        """
3892        if self.accountId is None or not self.accountId:
3893            uLogger.error("Variable `accountId` must be defined for using this method!")
3894            raise Exception("Account ID required")
3895
3896        uLogger.debug("Requesting current available funds for withdrawal. Wait, please...")
3897
3898        self.body = str({"accountId": self.accountId})
3899        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits"
3900        rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
3901
3902        if self.moreDebug:
3903            uLogger.debug("Records about available funds for withdrawal successfully received")
3904
3905        return rawLimits

Method for obtaining the available funds for withdrawal for current accountId.

See also:

Returns

dict with raw data from server that contains free funds for withdrawal. Example of dict: {"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}. Here money is an array of portfolio currency positions, blocked is an array of blocked currency positions of the portfolio and blockedGuarantee is locked money under collateral for futures.

def OverviewLimits(self, show: bool = False) -> dict:
3907    def OverviewLimits(self, show: bool = False) -> dict:
3908        """
3909        Method for parsing and show table with available funds for withdrawal for current `accountId`.
3910
3911        See also: `RequestLimits()`.
3912
3913        :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log.
3914        :return: dict with raw parsed data from server and some calculated statistics about it.
3915        """
3916        if self.accountId is None or not self.accountId:
3917            uLogger.error("Variable `accountId` must be defined for using this method!")
3918            raise Exception("Account ID required")
3919
3920        rawLimits = self.RequestLimits()  # raw response with current available funds for withdrawal
3921
3922        view = {
3923            "rawLimits": rawLimits,
3924            "limits": {  # parsed data for every currency:
3925                "money": {  # this is an array of portfolio currency positions
3926                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"]
3927                },
3928                "blocked": {  # this is an array of blocked currency
3929                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"]
3930                },
3931                "blockedGuarantee": {  # this is locked money under collateral for futures
3932                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"]
3933                },
3934            },
3935        }
3936
3937        # --- Prepare text table with limits in human-readable format:
3938        if show:
3939            info = [
3940                "# Withdrawal limits\n\n",
3941                "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
3942                "* **Account ID:** [{}]\n".format(self.accountId),
3943            ]
3944
3945            if view["limits"]["money"]:
3946                info.extend([
3947                    "\n| Currencies | Total         | Available for withdrawal | Blocked for trade | Futures guarantee |\n",
3948                    "|------------|---------------|--------------------------|-------------------|-------------------|\n",
3949                ])
3950
3951            else:
3952                info.append("\nNo withdrawal limits\n")
3953
3954            for curr in view["limits"]["money"].keys():
3955                blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0
3956                blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0
3957                availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee)
3958
3959                infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format(
3960                    "[{}]".format(curr),
3961                    "{:.2f}".format(view["limits"]["money"][curr]),
3962                    "{:.2f}".format(availableMoney),
3963                    "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—",
3964                    "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—",
3965                )
3966
3967                if curr == "rub":
3968                    info.insert(5, infoStr)  # hack: insert "rub" at the first position in table and after headers
3969
3970                else:
3971                    info.append(infoStr)
3972
3973            infoText = "".join(info)
3974
3975            uLogger.info(infoText)
3976
3977            if self.withdrawalLimitsFile:
3978                with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH:
3979                    fH.write(infoText)
3980
3981                uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile)))
3982
3983                if self.useHTMLReports:
3984                    htmlFilePath = self.withdrawalLimitsFile.replace(".md", ".html") if self.withdrawalLimitsFile.endswith(".md") else self.withdrawalLimitsFile + ".html"
3985                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
3986                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Withdrawal limits", commonCSS=COMMON_CSS, markdown=infoText))
3987
3988                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
3989
3990        return view

Method for parsing and show table with available funds for withdrawal for current accountId.

See also: RequestLimits().

Parameters
  • show: if False then only dictionary returns, if True then also print withdrawal limits to log.
Returns

dict with raw parsed data from server and some calculated statistics about it.

def RequestAccounts(self) -> dict:
3992    def RequestAccounts(self) -> dict:
3993        """
3994        Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`.
3995
3996        See also:
3997        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts
3998        - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account
3999        - `OverviewUserInfo()` method
4000
4001        :return: dict with raw data from server that contains accounts info. Example of dict:
4002                 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account",
4003                   "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z",
4004                   "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`.
4005                 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now.
4006        """
4007        uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...")
4008
4009        self.body = str({})
4010        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts"
4011        rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST")
4012
4013        if self.moreDebug:
4014            uLogger.debug("Records about available accounts successfully received")
4015
4016        return rawAccounts

Method for requesting all brokerage accounts (accountIds) of current user detected by token.

See also:

Returns

dict with raw data from server that contains accounts info. Example of dict: {"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}. If closedDate="1970-01-01T00:00:00Z" it means that account is active now.

def RequestUserInfo(self) -> dict:
4018    def RequestUserInfo(self) -> dict:
4019        """
4020        Method for requesting common user's information.
4021
4022        See also:
4023        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo
4024        - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest
4025        - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with
4026        - `OverviewUserInfo()` method
4027
4028        :return: dict with raw data from server that contains user's information. Example of dict:
4029                 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage",
4030                   "russian_shares", "structured_income_bonds"], "tariff": "premium"}`.
4031        """
4032        uLogger.debug("Requesting common user's information. Wait, please...")
4033
4034        self.body = str({})
4035        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo"
4036        rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST")
4037
4038        if self.moreDebug:
4039            uLogger.debug("Records about current user successfully received")
4040
4041        return rawUserInfo

Method for requesting common user's information.

See also:

Returns

dict with raw data from server that contains user's information. Example of dict: {"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", "russian_shares", "structured_income_bonds"], "tariff": "premium"}.

def RequestMarginStatus(self, accountId: str = None) -> dict:
4043    def RequestMarginStatus(self, accountId: str = None) -> dict:
4044        """
4045        Method for requesting margin calculation for defined account ID.
4046
4047        See also:
4048        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes
4049        - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse
4050        - `OverviewUserInfo()` method
4051
4052        :param accountId: string with numeric account ID. If `None`, then used class field `accountId`.
4053        :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict.
4054                 Example of responses:
4055                 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`.
4056                 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000},
4057                                    "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000},
4058                                    "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000},
4059                                    "fundsSufficiencyLevel": {"units": "1", "nano": 280000000},
4060                                    "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`.
4061        """
4062        if accountId is None or not accountId:
4063            if self.accountId is None or not self.accountId:
4064                uLogger.error("Variable `accountId` must be defined for using this method!")
4065                raise Exception("Account ID required")
4066
4067            else:
4068                accountId = self.accountId  # use `self.accountId` (main ID) by default
4069
4070        uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId))
4071
4072        self.body = str({"accountId": accountId})
4073        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes"
4074        rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST")
4075
4076        if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}:
4077            uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId))
4078            rawMargin = {}
4079
4080        else:
4081            if self.moreDebug:
4082                uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId))
4083
4084        return rawMargin

Method for requesting margin calculation for defined account ID.

See also:

Parameters
  • accountId: string with numeric account ID. If None, then used class field accountId.
Returns

dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. Example of responses: status code 400: {"code": 3, "message": "account margin status is disabled", "description": "30051" }, returns: {}. status code 200: {"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}.

def RequestTariffLimits(self) -> dict:
4086    def RequestTariffLimits(self) -> dict:
4087        """
4088        Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`.
4089
4090        See also:
4091        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff
4092        - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest
4093        - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit
4094        - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit
4095        - `OverviewUserInfo()` method
4096
4097        :return: dict with raw data from server that contains limits of current tariff. Example of dict:
4098                 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...],
4099                   "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`.
4100        """
4101        uLogger.debug("Requesting limits of current tariff. Wait, please...")
4102
4103        self.body = str({})
4104        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff"
4105        rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
4106
4107        if self.moreDebug:
4108            uLogger.debug("Records with limits of current tariff successfully received")
4109
4110        return rawTariffLimits

Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by token.

See also:

Returns

dict with raw data from server that contains limits of current tariff. Example of dict: {"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}.

def RequestBondCoupons(self, iJSON: dict) -> dict:
4112    def RequestBondCoupons(self, iJSON: dict) -> dict:
4113        """
4114        Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown
4115        then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`.
4116        All dates are in UTC timezone.
4117
4118        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons
4119        Documentation:
4120        - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest
4121        - response: https://tinkoff.github.io/investAPI/instruments/#coupon
4122
4123        See also: `ExtendBondsData()`.
4124
4125        :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self._ticker]`
4126                      If raw iJSON is not data of bond then server returns an error [400] with message:
4127                      `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`.
4128        :return: dictionary with bond payment calendar. Response example
4129                 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12",
4130                   "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000},
4131                   "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z",
4132                   "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}`
4133        """
4134        if iJSON["figi"] is None or not iJSON["figi"]:
4135            uLogger.error("FIGI must be defined for using this method!")
4136            raise Exception("FIGI required")
4137
4138        startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z"
4139        endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z"
4140
4141        uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format(
4142            "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "",
4143            self._figi,
4144            startDate,
4145            endDate,
4146        ))
4147
4148        self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate})
4149        calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons"
4150        calendar = self.SendAPIRequest(calendarURL, reqType="POST")
4151
4152        if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}:
4153            uLogger.warning("Instrument type is not bond!")
4154
4155        else:
4156            if self.moreDebug:
4157                uLogger.debug("Records about bond payment calendar successfully received")
4158
4159        return calendar

Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown then requesting dates "from": "1970-01-01T00:00:00.000Z" and "to": "2099-12-31T23:59:59.000Z". All dates are in UTC timezone.

REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons Documentation:

See also: ExtendBondsData().

Parameters
  • iJSON: raw json data of a bond from broker server, example iJSON = self.iList["Bonds"][self._ticker] If raw iJSON is not data of bond then server returns an error [400] with message: {"code": 3, "message": "instrument type is not bond", "description": "30048"}.
Returns

dictionary with bond payment calendar. Response example {"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}

def ExtendBondsData( self, instruments: list[str], xlsx: bool = False) -> pandas.core.frame.DataFrame:
4161    def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame:
4162        """
4163        Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider
4164        Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar,
4165        coupon yields, current yields and some statistics etc.
4166
4167        WARNING! This is too long operation if a lot of bonds requested from broker server.
4168
4169        See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`.
4170
4171        :param instruments: list of strings with tickers or FIGIs.
4172        :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`,
4173                     for further used by data scientists or stock analytics.
4174        :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker.
4175                 In XLSX-file and Pandas DataFrame fields mean:
4176                 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond
4177                 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon
4178        """
4179        if instruments is None or not instruments:
4180            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
4181            raise Exception("Ticker or FIGI required")
4182
4183        if isinstance(instruments, str):
4184            instruments = [instruments]
4185
4186        uniqueInstruments = self.GetUniqueFIGIs(instruments)
4187
4188        uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...")
4189
4190        iCount = len(uniqueInstruments)
4191        tooLong = iCount >= 20
4192        if tooLong:
4193            uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...")
4194
4195        bonds = None
4196        for i, self._figi in enumerate(uniqueInstruments):
4197            instrument = self.SearchByFIGI(requestPrice=False)  # raw data about instrument from server
4198
4199            if "type" in instrument.keys() and instrument["type"] == "Bonds":
4200                # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond
4201                rawBond = self.SearchByFIGI(requestPrice=True)
4202
4203                # Widen raw data with UTC current time (iData["actualDateTime"]):
4204                actualDate = datetime.now(tzutc())
4205                iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond
4206
4207                # Widen raw data with bond payment calendar (iData["rawCalendar"]):
4208                iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)}
4209
4210                # Replace some values with human-readable:
4211                iData["nominalCurrency"] = iData["nominal"]["currency"]
4212                iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"])
4213                iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"])
4214                iData["aciCurrency"] = iData["aciValue"]["currency"]
4215                iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"])
4216                iData["issueSize"] = int(iData["issueSize"])
4217                iData["issueSizePlan"] = int(iData["issueSizePlan"])
4218                iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]]
4219                iData["step"] = iData["step"] if "step" in iData.keys() else 0
4220                iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]]
4221                iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0
4222                iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0
4223                iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0
4224                iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0
4225                iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0
4226                iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0
4227
4228                # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date):
4229                iData["limitUpPercent"] = iData["currentPrice"]["limitUp"]  # max price on current day in percents of nominal
4230                iData["limitDownPercent"] = iData["currentPrice"]["limitDown"]  # min price on current day in percents of nominal
4231                iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"]  # last price on market in percents of nominal
4232                iData["closePricePercent"] = iData["currentPrice"]["closePrice"]  # previous day close in percents of nominal
4233                iData["changes"] = iData["currentPrice"]["changes"]  # this is percent of changes between `currentPrice` and `lastPrice`
4234                iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100  # max price on current day is `limitUpPercent` * `nominal`
4235                iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100  # min price on current day is `limitDownPercent` * `nominal`
4236                iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100  # last price on market is `lastPricePercent` * `nominal`
4237                iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100  # previous day close is `closePricePercent` * `nominal`
4238                iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"]  # this is delta between last deal price and last close
4239
4240                # Widen raw data with calendar data from `rawCalendar` values:
4241                calendarData = []
4242                if "events" in iData["rawCalendar"].keys():
4243                    for item in iData["rawCalendar"]["events"]:
4244                        calendarData.append({
4245                            "couponDate": item["couponDate"],
4246                            "couponNumber": int(item["couponNumber"]),
4247                            "fixDate": item["fixDate"] if "fixDate" in item.keys() else "",
4248                            "payCurrency": item["payOneBond"]["currency"],
4249                            "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]),
4250                            "couponType": TKS_COUPON_TYPES[item["couponType"]],
4251                            "couponStartDate": item["couponStartDate"],
4252                            "couponEndDate": item["couponEndDate"],
4253                            "couponPeriod": item["couponPeriod"],
4254                        })
4255
4256                    # if maturity date is unknown then uses the latest date in bond payment calendar for it:
4257                    if "maturityDate" not in iData.keys():
4258                        iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else ""
4259
4260                # Widen raw data with Coupon Rate.
4261                # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%:
4262                iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData])
4263                iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData])
4264                iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0.
4265
4266                # Widen raw data with Yield to Maturity (YTM) on current date.
4267                # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%:
4268                maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None
4269                iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None
4270                iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate])
4271                iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"]  # sum of all last coupons minus current ACI value
4272                iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0.
4273
4274                iData["calendar"] = calendarData  # adds calendar at the end
4275
4276                # Remove not used data:
4277                iData.pop("uid")
4278                iData.pop("positionUid")
4279                iData.pop("currentPrice")
4280                iData.pop("rawCalendar")
4281
4282                colNames = list(iData.keys())
4283                if bonds is None:
4284                    bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames))
4285
4286                else:
4287                    bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True)
4288
4289            else:
4290                uLogger.warning("Instrument is not a bond!")
4291
4292            processed = round(100 * (i + 1) / iCount, 1)
4293            if tooLong and processed % 5 == 0:
4294                uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount))
4295
4296            else:
4297                uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount))
4298
4299        bonds.index = bonds["ticker"].tolist()  # replace indexes with ticker names
4300
4301        # Saving bonds from Pandas DataFrame to XLSX sheet:
4302        if xlsx and self.bondsXLSXFile:
4303            with pd.ExcelWriter(
4304                    path=self.bondsXLSXFile,
4305                    date_format=TKS_DATE_FORMAT,
4306                    datetime_format=TKS_DATE_TIME_FORMAT,
4307                    mode="w",
4308            ) as writer:
4309                bonds.to_excel(
4310                    writer,
4311                    sheet_name="Extended bonds data",
4312                    index=True,
4313                    encoding="UTF-8",
4314                    freeze_panes=(1, 1),
4315                )  # saving as XLSX-file with freeze first row and column as headers
4316
4317            uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile)))
4318
4319        return bonds

Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc.

WARNING! This is too long operation if a lot of bonds requested from broker server.

See also: ShowInstrumentInfo(), CreateBondsCalendar(), ShowBondsCalendar(), RequestBondCoupons().

Parameters
  • instruments: list of strings with tickers or FIGIs.
  • xlsx: if True then also exports Pandas DataFrame to xlsx-file bondsXLSXFile, default ext-bonds.xlsx, for further used by data scientists or stock analytics.
Returns

wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker. In XLSX-file and Pandas DataFrame fields mean: - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon

def CreateBondsCalendar( self, extBonds: pandas.core.frame.DataFrame, xlsx: bool = False) -> pandas.core.frame.DataFrame:
4321    def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame:
4322        """
4323        Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default.
4324
4325        WARNING! This is too long operation if a lot of bonds requested from broker server.
4326
4327        See also: `ShowBondsCalendar()`, `ExtendBondsData()`.
4328
4329        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
4330                        extended information about bonds: main info, current prices, bond payment calendar,
4331                        coupon yields, current yields and some statistics etc.
4332                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4333        :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default,
4334                     for further used by data scientists or stock analytics.
4335        :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon
4336        """
4337        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4338            extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=False)
4339
4340        uLogger.debug("Generating bond payments calendar data. Wait, please...")
4341
4342        colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"]
4343        colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"]
4344        calendar = None
4345        for bond in extBonds.iterrows():
4346            for item in bond[1]["calendar"]:
4347                cData = {
4348                    "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()),
4349                    "couponDate": item["couponDate"],
4350                    "figi": bond[1]["figi"],
4351                    "ticker": bond[1]["ticker"],
4352                    "name": bond[1]["name"],
4353                    "couponNumber": item["couponNumber"],
4354                    "payOneBond": item["payOneBond"],
4355                    "payCurrency": item["payCurrency"],
4356                    "couponType": item["couponType"],
4357                    "couponPeriod": item["couponPeriod"],
4358                    "fixDate": item["fixDate"],
4359                    "couponStartDate": item["couponStartDate"],
4360                    "couponEndDate": item["couponEndDate"],
4361                }
4362
4363                if calendar is None:
4364                    calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID))
4365
4366                else:
4367                    calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True)
4368
4369        if calendar is not None:
4370            calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True)  # sort all payments for all bonds by payment date
4371
4372            # Saving calendar from Pandas DataFrame to XLSX sheet:
4373            if xlsx:
4374                xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx"
4375
4376                with pd.ExcelWriter(
4377                        path=xlsxCalendarFile,
4378                        date_format=TKS_DATE_FORMAT,
4379                        datetime_format=TKS_DATE_TIME_FORMAT,
4380                        mode="w",
4381                ) as writer:
4382                    humanReadable = calendar.copy(deep=True)
4383                    humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0])
4384                    humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0])
4385                    humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0])
4386                    humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0])
4387                    humanReadable.columns = colNames  # human-readable column names
4388
4389                    humanReadable.to_excel(
4390                        writer,
4391                        sheet_name="Bond payments calendar",
4392                        index=False,
4393                        encoding="UTF-8",
4394                        freeze_panes=(1, 2),
4395                    )  # saving as XLSX-file with freeze first row and column as headers
4396
4397                    del humanReadable  # release df in memory
4398
4399                uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile)))
4400
4401        return calendar

Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, calendar.xlsx by default.

WARNING! This is too long operation if a lot of bonds requested from broker server.

See also: ShowBondsCalendar(), ExtendBondsData().

Parameters
  • extBonds: Pandas DataFrame object returns by ExtendBondsData() method and contains extended information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc. If this parameter is None then used figi or ticker as bond name and then calculate ExtendBondsData().
  • xlsx: if True then also exports Pandas DataFrame to file calendarFile + ".xlsx", calendar.xlsx by default, for further used by data scientists or stock analytics.
Returns

Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon

def ShowBondsCalendar(self, extBonds: pandas.core.frame.DataFrame, show: bool = True) -> str:
4403    def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True) -> str:
4404        """
4405        Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond.
4406        Also, creates Markdown file with calendar data, `calendar.md` by default.
4407
4408        See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`.
4409
4410        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
4411                        extended information about bonds: main info, current prices, bond payment calendar,
4412                        coupon yields, current yields and some statistics etc.
4413                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4414        :param show: if `True` then also printing bonds payment calendar to the console,
4415                     otherwise save to file `calendarFile` only. `False` by default.
4416        :return: multilines text in Markdown format with bonds payment calendar as a table.
4417        """
4418        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4419            extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=False)
4420
4421        infoText = "# Bond payments calendar\n\n"
4422
4423        calendar = self.CreateBondsCalendar(extBonds, xlsx=True)  # generate Pandas DataFrame with full calendar data
4424
4425        if not (calendar is None or calendar.empty):
4426            splitLine = "|       |                 |              |              |     |               |           |        |                   |\n"
4427
4428            info = [
4429                "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4430                "| Paid  | Payment date    | FIGI         | Ticker       | No. | Value         | Type      | Period | End registry date |\n",
4431                "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n",
4432            ]
4433
4434            newMonth = False
4435            notOneBond = calendar["figi"].nunique() > 1
4436            for i, bond in enumerate(calendar.iterrows()):
4437                if newMonth and notOneBond:
4438                    info.append(splitLine)
4439
4440                info.append(
4441                    "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format(
4442                        "  √" if bond[1]["paid"] else "  —",
4443                        bond[1]["couponDate"].split("T")[0],
4444                        bond[1]["figi"],
4445                        bond[1]["ticker"],
4446                        bond[1]["couponNumber"],
4447                        "{} {}".format(
4448                            "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."),
4449                            bond[1]["payCurrency"],
4450                        ),
4451                        bond[1]["couponType"],
4452                        bond[1]["couponPeriod"],
4453                        bond[1]["fixDate"].split("T")[0],
4454                    )
4455                )
4456
4457                if i < len(calendar.values) - 1:
4458                    curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4459                    nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4460                    newMonth = False if curDate.month == nextDate.month else True
4461
4462                else:
4463                    newMonth = False
4464
4465            infoText += "".join(info)
4466
4467            if show:
4468                uLogger.info("{}".format(infoText))
4469
4470            if self.calendarFile is not None:
4471                with open(self.calendarFile, "w", encoding="UTF-8") as fH:
4472                    fH.write(infoText)
4473
4474                uLogger.info("Bond payments calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile)))
4475
4476                if self.useHTMLReports:
4477                    htmlFilePath = self.calendarFile.replace(".md", ".html") if self.calendarFile.endswith(".md") else self.calendarFile + ".html"
4478                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
4479                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Bond payments calendar", commonCSS=COMMON_CSS, markdown=infoText))
4480
4481                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
4482
4483        else:
4484            infoText += "No data\n"
4485
4486        return infoText

Show bond payments calendar as a table. One row in input bonds dataframe contains one bond. Also, creates Markdown file with calendar data, calendar.md by default.

See also: ShowInstrumentInfo(), RequestBondCoupons(), CreateBondsCalendar() and ExtendBondsData().

Parameters
  • extBonds: Pandas DataFrame object returns by ExtendBondsData() method and contains extended information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc. If this parameter is None then used figi or ticker as bond name and then calculate ExtendBondsData().
  • show: if True then also printing bonds payment calendar to the console, otherwise save to file calendarFile only. False by default.
Returns

multilines text in Markdown format with bonds payment calendar as a table.

def OverviewAccounts(self, show: bool = False) -> dict:
4488    def OverviewAccounts(self, show: bool = False) -> dict:
4489        """
4490        Method for parsing and show simple table with all available user accounts.
4491
4492        See also: `RequestAccounts()` and `OverviewUserInfo()` methods.
4493
4494        :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log.
4495        :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict:
4496                 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...},
4497                          "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1",
4498                                                        "status": "Opened and active account", "opened": "2018-05-23 00:00:00",
4499                                                        "closed": "—", "access": "Full access" }, ...}}`
4500        """
4501        rawAccounts = self.RequestAccounts()  # Raw responses with accounts
4502
4503        # This is an array of dict with user accounts, its `accountId`s and some parsed data:
4504        accounts = {
4505            item["id"]: {
4506                "type": TKS_ACCOUNT_TYPES[item["type"]],
4507                "name": item["name"],
4508                "status": TKS_ACCOUNT_STATUSES[item["status"]],
4509                "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4510                "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—",
4511                "access": TKS_ACCESS_LEVELS[item["accessLevel"]],
4512            } for item in rawAccounts["accounts"]
4513        }
4514
4515        # Raw and parsed data with some fields replaced in "stat" section:
4516        view = {
4517            "rawAccounts": rawAccounts,
4518            "stat": accounts,
4519        }
4520
4521        # --- Prepare simple text table with only accounts data in human-readable format:
4522        if show:
4523            info = [
4524                "# User accounts\n\n",
4525                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4526                "| Account ID   | Type                      | Status                    | Name                           |\n",
4527                "|--------------|---------------------------|---------------------------|--------------------------------|\n",
4528            ]
4529
4530            for account in view["stat"].keys():
4531                info.extend([
4532                    "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format(
4533                        account,
4534                        view["stat"][account]["type"],
4535                        view["stat"][account]["status"],
4536                        view["stat"][account]["name"],
4537                    )
4538                ])
4539
4540            infoText = "".join(info)
4541
4542            uLogger.info(infoText)
4543
4544            if self.userAccountsFile:
4545                with open(self.userAccountsFile, "w", encoding="UTF-8") as fH:
4546                    fH.write(infoText)
4547
4548                uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile)))
4549
4550                if self.useHTMLReports:
4551                    htmlFilePath = self.userAccountsFile.replace(".md", ".html") if self.userAccountsFile.endswith(".md") else self.userAccountsFile + ".html"
4552                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
4553                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User accounts", commonCSS=COMMON_CSS, markdown=infoText))
4554
4555                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
4556
4557        return view

Method for parsing and show simple table with all available user accounts.

See also: RequestAccounts() and OverviewUserInfo() methods.

Parameters
  • show: if False then only dictionary with accounts data returns, if True then also print it to log.
Returns

dict with parsed accounts data received from RequestAccounts() method. Example of dict: view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", "status": "Opened and active account", "opened": "2018-05-23 00:00:00", "closed": "—", "access": "Full access" }, ...}}

def OverviewUserInfo(self, show: bool = False) -> dict:
4559    def OverviewUserInfo(self, show: bool = False) -> dict:
4560        """
4561        Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit).
4562
4563        See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods.
4564
4565        :param show: if `False` then only dictionary returns, if `True` then also print user's data to log.
4566        :return: dict with raw parsed data from server and some calculated statistics about it.
4567        """
4568        overview = self.Overview(show=False)  # Request current user portfolio for the ability to calculate missing funds
4569        tmpTicker = self._ticker
4570        self._ticker = "RUB000UTSTOM"  # This instrument show in rub how much money cost current margin
4571        missing = self.GetInstrumentFromPortfolio(portfolio=overview)
4572        self._ticker = tmpTicker
4573
4574        rawUserInfo = self.RequestUserInfo()  # Raw response with common user info
4575        overviewAccount = self.OverviewAccounts(show=False)  # Raw and parsed accounts data
4576        rawAccounts = overviewAccount["rawAccounts"]  # Raw response with user accounts data
4577        accounts = overviewAccount["stat"]  # Dict with only statistics about user accounts
4578        rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()}  # Raw response with margin calculation for every account ID
4579        rawTariffLimits = self.RequestTariffLimits()  # Raw response with limits of current tariff
4580
4581        # This is dict with parsed common user data:
4582        userInfo = {
4583            "premium": "Yes" if rawUserInfo["premStatus"] else "No",
4584            "qualified": "Yes" if rawUserInfo["qualStatus"] else "No",
4585            "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]],
4586            "tariff": rawUserInfo["tariff"],
4587        }
4588
4589        # This is an array of dict with parsed margin statuses for every account IDs:
4590        margins = {}
4591        for accountId in accounts.keys():
4592            if rawMargins[accountId]:
4593                margins[accountId] = {
4594                    "currency": rawMargins[accountId]["liquidPortfolio"]["currency"],
4595                    "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]),
4596                    "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]),
4597                    "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]),
4598                    "diff": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]),
4599                    "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]),
4600                    "missing": missing["volume"],
4601                }
4602
4603            else:
4604                margins[accountId] = {}  # Server response: margin status is disabled for current accountId
4605
4606        unary = {}  # unary-connection limits
4607        for item in rawTariffLimits["unaryLimits"]:
4608            if item["limitPerMinute"] in unary.keys():
4609                unary[item["limitPerMinute"]].extend(item["methods"])
4610
4611            else:
4612                unary[item["limitPerMinute"]] = item["methods"]
4613
4614        stream = {}  # stream-connection limits
4615        for item in rawTariffLimits["streamLimits"]:
4616            if item["limit"] in stream.keys():
4617                stream[item["limit"]].extend(item["streams"])
4618
4619            else:
4620                stream[item["limit"]] = item["streams"]
4621
4622        # This is dict with parsed limits of current tariff (connections, API methods etc.):
4623        limits = {
4624            "unary": unary,
4625            "stream": stream,
4626        }
4627
4628        # Raw and parsed data as an output result:
4629        view = {
4630            "rawUserInfo": rawUserInfo,
4631            "rawAccounts": rawAccounts,
4632            "rawMargins": rawMargins,
4633            "rawTariffLimits": rawTariffLimits,
4634            "stat": {
4635                "overview": overview,
4636                "userInfo": userInfo,
4637                "accounts": accounts,
4638                "margins": margins,
4639                "limits": limits,
4640            },
4641        }
4642
4643        # --- Prepare text table with user information in human-readable format:
4644        if show:
4645            info = [
4646                "# Full user information\n\n",
4647                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4648                "## Common information\n\n",
4649                "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]),
4650                "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]),
4651                "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]),
4652                "* **Allowed to work with instruments:**\n{}\n".format("".join(["  - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])),
4653                "\n## User accounts\n\n",
4654            ]
4655
4656            for account in view["stat"]["accounts"].keys():
4657                info.extend([
4658                    "### ID: [{}]\n\n".format(account),
4659                    "| Parameters           | Values                                                       |\n",
4660                    "|----------------------|--------------------------------------------------------------|\n",
4661                    "| Account type:        | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]),
4662                    "| Account name:        | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]),
4663                    "| Account status:      | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]),
4664                    "| Access level:        | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]),
4665                    "| Date opened:         | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]),
4666                    "| Date closed:         | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]),
4667                ])
4668
4669                if margins[account]:
4670                    info.extend([
4671                        "| Margin status:       | Enabled                                                      |\n",
4672                        "| - Liquid portfolio:  | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])),
4673                        "| - Margin starting:   | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])),
4674                        "| - Margin minimum:    | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])),
4675                        "| - Margin difference: | {:<60} |\n".format("{} {}".format(margins[account]["diff"], margins[account]["currency"])),
4676                        "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)),
4677                        "| - Not covered funds: | {:<60} |\n\n".format("{:.2f} {}".format(margins[account]["missing"], margins[account]["currency"])),
4678                    ])
4679
4680                else:
4681                    info.append("| Margin status:       | Disabled                                                     |\n\n")
4682
4683            info.extend([
4684                "\n## Current user tariff limits\n",
4685                "\n### See also\n",
4686                "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n",
4687                "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n",
4688                "  - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n",
4689                "  - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n",
4690                "\n### Unary limits\n",
4691            ])
4692
4693            if unary:
4694                for key, values in sorted(unary.items()):
4695                    info.append("\n* Max requests per minute: {}\n".format(key))
4696
4697                    for value in values:
4698                        info.append("  - {}\n".format(value))
4699
4700            else:
4701                info.append("\nNot available\n")
4702
4703            info.append("\n### Stream limits\n")
4704
4705            if stream:
4706                for key, values in sorted(stream.items()):
4707                    info.append("\n* Max stream connections: {}\n".format(key))
4708
4709                    for value in values:
4710                        info.append("  - {}\n".format(value))
4711
4712            else:
4713                info.append("\nNot available\n")
4714
4715            infoText = "".join(info)
4716
4717            uLogger.info(infoText)
4718
4719            if self.userInfoFile:
4720                with open(self.userInfoFile, "w", encoding="UTF-8") as fH:
4721                    fH.write(infoText)
4722
4723                uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile)))
4724
4725                if self.useHTMLReports:
4726                    htmlFilePath = self.userInfoFile.replace(".md", ".html") if self.userInfoFile.endswith(".md") else self.userInfoFile + ".html"
4727                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
4728                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User info", commonCSS=COMMON_CSS, markdown=infoText))
4729
4730                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
4731
4732        return view

Method for parsing and show all available user's data (accountIds, common user information, margin status and tariff connections limit).

See also: OverviewAccounts(), RequestAccounts(), RequestUserInfo(), RequestMarginStatus() and RequestTariffLimits() methods.

Parameters
  • show: if False then only dictionary returns, if True then also print user's data to log.
Returns

dict with raw parsed data from server and some calculated statistics about it.

class Args:
4735class Args:
4736    """
4737    If `Main()` function is imported as module, then this class used to convert arguments from **kwargs as object.
4738    """
4739    def __init__(self, **kwargs):
4740        self.__dict__.update(kwargs)
4741
4742    def __getattr__(self, item):
4743        return None

If Main() function is imported as module, then this class used to convert arguments from **kwargs as object.

Args(**kwargs)
4739    def __init__(self, **kwargs):
4740        self.__dict__.update(kwargs)
def ParseArgs():
4746def ParseArgs():
4747    """This function get and parse command line keys."""
4748    parser = ArgumentParser()  # command-line string parser
4749
4750    parser.description = "TKSBrokerAPI is a trading platform for automation on Python to simplify the implementation of trading scenarios and work with Tinkoff Invest API server via the REST protocol. See examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md"
4751    parser.usage = "\n/as module/ python TKSBrokerAPI.py [some options] [one command]\n/as CLI tool/ tksbrokerapi [some options] [one command]"
4752
4753    # --- options:
4754
4755    parser.add_argument("--no-cache", action="store_true", default=False, help="Option: not use local cache `dump.json`, but update raw instruments data when starting the platform. `False` by default.")
4756    parser.add_argument("--token", type=str, help="Option: Tinkoff service's api key. If not set then used environment variable `TKS_API_TOKEN`. See how to use: https://tinkoff.github.io/investAPI/token/")
4757    parser.add_argument("--account-id", type=str, default=None, help="Option: string with an user numeric account ID in Tinkoff Broker. It can be found in any broker's reports (see the contract number). Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.")
4758
4759    parser.add_argument("--ticker", "-t", type=str, help="Option: instrument's ticker, e.g. `IBM`, `YNDX`, `GOOGL` etc. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR`.")
4760    parser.add_argument("--figi", "-f", type=str, help="Option: instrument's FIGI, e.g. `BBG006L8G4H1` (for `YNDX`).")
4761
4762    parser.add_argument("--depth", type=int, default=1, help="Option: Depth of Market (DOM) can be >=1, 1 by default.")
4763    parser.add_argument("--no-cancelled", "--no-canceled", action="store_true", default=False, help="Option: remove information about cancelled operations from the deals report by the `--deals` key. `False` by default.")
4764
4765    parser.add_argument("--output", type=str, default=None, help="Option: replace default paths to output files for some commands. If `None` then used default files.")
4766    parser.add_argument("--html", "--HTML", action="store_true", default=False, help="Option: if key present then TKSBrokerAPI generate also HTML reports from Markdown. False by default.")
4767
4768    parser.add_argument("--interval", type=str, default="hour", help="Option: available values are `1min`, `5min`, `15min`, `hour` and `day`. Used only with `--history` key. This is time period of one candle. Default: `hour` for every history candles.")
4769    parser.add_argument("--only-missing", action="store_true", default=False, help="Option: if history file define by `--output` key then add only last missing candles, do not request all history length. `False` by default.")
4770    parser.add_argument("--csv-sep", type=str, default=",", help="Option: separator if csv-file is used, `,` by default.")
4771
4772    parser.add_argument("--debug-level", "--log-level", "--verbosity", "-v", type=int, default=20, help="Option: showing STDOUT messages of minimal debug level, e.g. 10 = DEBUG, 20 = INFO, 30 = WARNING, 40 = ERROR, 50 = CRITICAL. INFO (20) by default.")
4773    parser.add_argument("--more", "--more-debug", action="store_true", default=False, help="Option: `--debug-level` key only switch log level verbosity, but in addition `--more` key enable all debug information, such as net request and response headers in all methods.")
4774
4775    # --- commands:
4776
4777    parser.add_argument("--version", "--ver", action="store_true", help="Action: shows current semantic version, looks like `major.minor.buildnumber`. If TKSBrokerAPI not installed via pip, then used local build number `.dev0`.")
4778
4779    parser.add_argument("--list", "-l", action="store_true", help="Action: get and print all available instruments and some information from broker server. Also, you can define `--output` key to save list of instruments to file, default: `instruments.md`.")
4780    parser.add_argument("--list-xlsx", "-x", action="store_true", help="Action: get all available instruments from server for current account and save raw data into xlsx-file for further used by data scientists or stock analytics, default: `dump.xlsx`.")
4781    parser.add_argument("--bonds-xlsx", "-b", type=str, nargs="*", help="Action: get all available bonds if only key present or list of bonds with FIGIs or tickers and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bonds payment calendar, coupon yields, current yields and some statistics etc. And then export data to XLSX-file, default: `ext-bonds.xlsx` or you can change it with `--output` key. WARNING! This is too long operation if a lot of bonds requested from broker server.")
4782    parser.add_argument("--search", "-s", type=str, nargs=1, help="Action: search for an instruments by part of the name, ticker or FIGI. Also, you can define `--output` key to save results to file, default: `search-results.md`.")
4783    parser.add_argument("--info", "-i", action="store_true", help="Action: get information from broker server about instrument by it's ticker or FIGI. `--ticker` key or `--figi` key must be defined!")
4784    parser.add_argument("--calendar", "-c", type=str, nargs="*", help="Action: show bonds payment calendar as a table. Calendar build for one or more tickers or FIGIs, or for all bonds if only key present. If the `--output` key present then calendar saves to file, default: `calendar.md`. Also, created XLSX-file with bond payments calendar for further used by data scientists or stock analytics, `calendar.xlsx` by default. WARNING! This is too long operation if a lot of bonds requested from broker server.")
4785    parser.add_argument("--price", action="store_true", help="Action: show actual price list for current instrument. Also, you can use `--depth` key. `--ticker` key or `--figi` key must be defined!")
4786    parser.add_argument("--prices", "-p", type=str, nargs="+", help="Action: get and print current prices for list of given instruments (by it's tickers or by FIGIs). WARNING! This is too long operation if you request a lot of instruments! Also, you can define `--output` key to save list of prices to file, default: `prices.md`.")
4787
4788    parser.add_argument("--overview", "-o", action="store_true", help="Action: shows all open positions, orders and some statistics. Also, you can define `--output` key to save this information to file, default: `overview.md`.")
4789    parser.add_argument("--overview-digest", action="store_true", help="Action: shows a short digest of the portfolio status. Also, you can define `--output` key to save this information to file, default: `overview-digest.md`.")
4790    parser.add_argument("--overview-positions", action="store_true", help="Action: shows only open positions. Also, you can define `--output` key to save this information to file, default: `overview-positions.md`.")
4791    parser.add_argument("--overview-orders", action="store_true", help="Action: shows only sections of open limits and stop orders. Also, you can define `--output` key to save orders to file, default: `overview-orders.md`.")
4792    parser.add_argument("--overview-analytics", action="store_true", help="Action: shows only the analytics section and the distribution of the portfolio by various categories. Also, you can define `--output` key to save this information to file, default: `overview-analytics.md`.")
4793    parser.add_argument("--overview-calendar", action="store_true", help="Action: shows only the bonds calendar section (if these present in portfolio). Also, you can define `--output` key to save this information to file, default: `overview-calendar.md`.")
4794
4795    parser.add_argument("--deals", "-d", type=str, nargs="*", help="Action: show all deals between two given dates. Start day may be an integer number: -1, -2, -3 days ago. Also, you can use keywords: `today`, `yesterday` (-1), `week` (-7), `month` (-30) and `year` (-365). Dates format must be: `%%Y-%%m-%%d`, e.g. 2020-02-03. With `--no-cancelled` key information about cancelled operations will be removed from the deals report. Also, you can define `--output` key to save all deals to file, default: `deals.md`.")
4796    parser.add_argument("--history", type=str, nargs="*", help="Action: get last history candles of the current instrument defined by `--ticker` or `--figi` (FIGI id) keys. History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. This action may be used together with the `--render-chart` key. Also, you can define `--output` key to save history candlesticks to file.")
4797    parser.add_argument("--load-history", type=str, help="Action: try to load history candles from given csv-file as a Pandas Dataframe and print it in to the console. This action may be used together with the `--render-chart` key.")
4798    parser.add_argument("--render-chart", type=str, help="Action: render candlesticks chart. This key may only used with `--history` or `--load-history` together. Action has 1 parameter with two possible string values: `interact` (`i`) or `non-interact` (`ni`).")
4799
4800    parser.add_argument("--trade", nargs="*", help="Action: universal action to open market position for defined ticker or FIGI. You must specify 1-5 parameters: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. See examples in readme.")
4801    parser.add_argument("--buy", nargs="*", help="Action: immediately open BUY market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].")
4802    parser.add_argument("--sell", nargs="*", help="Action: immediately open SELL market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].")
4803
4804    parser.add_argument("--order", nargs="*", help="Action: universal action to open limit or stop-order in any directions. You must specify 4-7 parameters: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]]. See examples in readme.")
4805    parser.add_argument("--buy-limit", type=float, nargs=2, help="Action: open pending BUY limit-order (below current price). You must specify only 2 parameters: [lots] [target price] to open BUY limit-order. If you try to create `Buy` limit-order above current price then broker immediately open `Buy` market order, such as if you do simple `--buy` operation!")
4806    parser.add_argument("--sell-limit", type=float, nargs=2, help="Action: open pending SELL limit-order (above current price). You must specify only 2 parameters: [lots] [target price] to open SELL limit-order. If you try to create `Sell` limit-order below current price then broker immediately open `Sell` market order, such as if you do simple `--sell` operation!")
4807    parser.add_argument("--buy-stop", nargs="*", help="Action: open BUY stop-order. You must specify at least 2 parameters: [lots] [target price] to open BUY stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.")
4808    parser.add_argument("--sell-stop", nargs="*", help="Action: open SELL stop-order. You must specify at least 2 parameters: [lots] [target price] to open SELL stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.")
4809    # parser.add_argument("--buy-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending BUY limit-orders (below current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!")
4810    # parser.add_argument("--sell-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending SELL limit-orders (above current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!")
4811
4812    parser.add_argument("--close-order", "--cancel-order", type=str, nargs=1, help="Action: close only one order by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.")
4813    parser.add_argument("--close-orders", "--cancel-orders", type=str, nargs="+", help="Action: close one or list of orders by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.")
4814    parser.add_argument("--close-trade", "--cancel-trade", action="store_true", help="Action: close only one position for instrument defined by `--ticker` (high priority) or `--figi` keys, including for currencies tickers.")
4815    parser.add_argument("--close-trades", "--cancel-trades", type=str, nargs="+", help="Action: close positions for list of tickers or FIGIs, including for currencies tickers or FIGIs.")
4816    parser.add_argument("--close-all", "--cancel-all", type=str, nargs="*", help="Action: close all available (not blocked) opened trades and orders, excluding for currencies. Also you can select one or more keywords case insensitive to specify trades type: `orders`, `shares`, `bonds`, `etfs` and `futures`, but not `currencies`. Currency positions you must closes manually using `--buy`, `--sell`, `--close-trade` or `--close-trades` operations. If the `--close-all` key present with the `--ticker` or `--figi` keys, then positions and all open limit and stop orders for the specified instrument are closed.")
4817
4818    parser.add_argument("--limits", "--withdrawal-limits", "-w", action="store_true", help="Action: show table of funds available for withdrawal for current `accountId`. You can change `accountId` with the key `--account-id`. Also, you can define `--output` key to save this information to file, default: `limits.md`.")
4819    parser.add_argument("--user-info", "-u", action="store_true", help="Action: show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). Also, you can define `--output` key to save this information to file, default: `user-info.md`.")
4820    parser.add_argument("--account", "--accounts", "-a", action="store_true", help="Action: show simple table with all available user accounts. Also, you can define `--output` key to save this information to file, default: `accounts.md`.")
4821
4822    cmdArgs = parser.parse_args()
4823    return cmdArgs

This function get and parse command line keys.

def Main(**kwargs):
4826def Main(**kwargs):
4827    """
4828    Main function for work with TKSBrokerAPI in the console.
4829
4830    See examples:
4831    - in english: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md
4832    - in russian: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README.md
4833    """
4834    args = Args(**kwargs) if kwargs else ParseArgs()  # get and parse command-line parameters or use **kwarg parameters
4835
4836    if args.debug_level:
4837        uLogger.level = 10  # always debug level by default
4838        uLogger.handlers[0].level = args.debug_level  # level for STDOUT
4839
4840    exitCode = 0
4841    start = datetime.now(tzutc())
4842    uLogger.debug("=-" * 50)
4843    uLogger.debug(">>> TKSBrokerAPI module started at: [{}] UTC, it is [{}] local time".format(
4844        start.strftime(TKS_PRINT_DATE_TIME_FORMAT),
4845        start.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4846    ))
4847
4848    # trying to calculate full current version:
4849    buildVersion = __version__
4850    try:
4851        v = version("tksbrokerapi")
4852        buildVersion = v if v.startswith(buildVersion) else buildVersion + ".dev0"  # set version as major.minor.dev0 if run as local build or local script
4853
4854    except Exception:
4855        buildVersion = __version__ + ".dev0"  # if an errors occurred then also set version as major.minor.dev0
4856
4857    uLogger.debug("TKSBrokerAPI major.minor.build version used: [{}]".format(buildVersion))
4858    uLogger.debug("Host CPU count: [{}]".format(CPU_COUNT))
4859
4860    try:
4861        if args.version:
4862            print("TKSBrokerAPI {}".format(buildVersion))
4863            uLogger.debug("User requested current TKSBrokerAPI major.minor.build version: [{}]".format(buildVersion))
4864
4865        else:
4866            # Init class for trading with Tinkoff Broker:
4867            trader = TinkoffBrokerServer(
4868                token=args.token,
4869                accountId=args.account_id,
4870                useCache=not args.no_cache,
4871            )
4872
4873            # --- set some options:
4874
4875            if args.more:
4876                trader.moreDebug = True
4877                uLogger.warning("More debug info mode is enabled! See network requests, responses and its headers in the full log or run TKSBrokerAPI platform with the `--verbosity 10` to show theres in console.")
4878
4879            if args.html:
4880                trader.useHTMLReports = True
4881
4882            if args.ticker:
4883                ticker = str(args.ticker).upper()  # Tickers may be upper case only
4884
4885                if ticker in trader.aliasesKeys:
4886                    trader.ticker = trader.aliases[ticker]  # Replace some tickers with its aliases
4887
4888                else:
4889                    trader.ticker = ticker
4890
4891            if args.figi:
4892                trader.figi = str(args.figi).upper()  # FIGIs may be upper case only
4893
4894            if args.depth is not None:
4895                trader.depth = args.depth
4896
4897            # --- do one command:
4898
4899            if args.list:
4900                if args.output is not None:
4901                    trader.instrumentsFile = args.output
4902
4903                trader.ShowInstrumentsInfo(show=True)
4904
4905            elif args.list_xlsx:
4906                trader.DumpInstrumentsAsXLSX(forceUpdate=False)
4907
4908            elif args.bonds_xlsx is not None:
4909                if args.output is not None:
4910                    trader.bondsXLSXFile = args.output
4911
4912                if len(args.bonds_xlsx) == 0:
4913                    trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=True)  # request bonds with all available tickers
4914
4915                else:
4916                    trader.ExtendBondsData(instruments=args.bonds_xlsx, xlsx=True)  # request list of given bonds
4917
4918            elif args.search:
4919                if args.output is not None:
4920                    trader.searchResultsFile = args.output
4921
4922                trader.SearchInstruments(pattern=args.search[0], show=True)
4923
4924            elif args.info:
4925                if not (args.ticker or args.figi):
4926                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
4927                    raise Exception("Ticker or FIGI required")
4928
4929                if args.output is not None:
4930                    trader.infoFile = args.output
4931
4932                if args.ticker:
4933                    trader.SearchByTicker(requestPrice=True, show=True)  # show info and current prices by ticker name
4934
4935                else:
4936                    trader.SearchByFIGI(requestPrice=True, show=True)  # show info and current prices by FIGI id
4937
4938            elif args.calendar is not None:
4939                if args.output is not None:
4940                    trader.calendarFile = args.output
4941
4942                if len(args.calendar) == 0:
4943                    bondsData = trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=False)  # request bonds with all available tickers
4944
4945                else:
4946                    bondsData = trader.ExtendBondsData(instruments=args.calendar, xlsx=False)  # request list of given bonds
4947
4948                trader.ShowBondsCalendar(extBonds=bondsData, show=True)  # shows bonds payment calendar only
4949
4950            elif args.price:
4951                if not (args.ticker or args.figi):
4952                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
4953                    raise Exception("Ticker or FIGI required")
4954
4955                trader.GetCurrentPrices(show=True)
4956
4957            elif args.prices is not None:
4958                if args.output is not None:
4959                    trader.pricesFile = args.output
4960
4961                trader.GetListOfPrices(instruments=args.prices, show=True)  # WARNING: too long wait for a lot of instruments prices
4962
4963            elif args.overview:
4964                if args.output is not None:
4965                    trader.overviewFile = args.output
4966
4967                trader.Overview(show=True, details="full")
4968
4969            elif args.overview_digest:
4970                if args.output is not None:
4971                    trader.overviewDigestFile = args.output
4972
4973                trader.Overview(show=True, details="digest")
4974
4975            elif args.overview_positions:
4976                if args.output is not None:
4977                    trader.overviewPositionsFile = args.output
4978
4979                trader.Overview(show=True, details="positions")
4980
4981            elif args.overview_orders:
4982                if args.output is not None:
4983                    trader.overviewOrdersFile = args.output
4984
4985                trader.Overview(show=True, details="orders")
4986
4987            elif args.overview_analytics:
4988                if args.output is not None:
4989                    trader.overviewAnalyticsFile = args.output
4990
4991                trader.Overview(show=True, details="analytics")
4992
4993            elif args.overview_calendar:
4994                if args.output is not None:
4995                    trader.overviewAnalyticsFile = args.output
4996
4997                trader.Overview(show=True, details="calendar")
4998
4999            elif args.deals is not None:
5000                if args.output is not None:
5001                    trader.reportFile = args.output
5002
5003                if 0 <= len(args.deals) < 3:
5004                    trader.Deals(
5005                        start=args.deals[0] if len(args.deals) >= 1 else None,
5006                        end=args.deals[1] if len(args.deals) == 2 else None,
5007                        show=True,  # Always show deals report in console
5008                        showCancelled=not args.no_cancelled,  # If --no-cancelled key then remove cancelled operations from the deals report. False by default.
5009                    )
5010
5011                else:
5012                    uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]")
5013                    raise Exception("Incorrect value")
5014
5015            elif args.history is not None:
5016                if args.output is not None:
5017                    trader.historyFile = args.output
5018
5019                if 0 <= len(args.history) < 3:
5020                    dataReceived = trader.History(
5021                        start=args.history[0] if len(args.history) >= 1 else None,
5022                        end=args.history[1] if len(args.history) == 2 else None,
5023                        interval="hour" if args.interval is None or not args.interval else args.interval,
5024                        onlyMissing=False if args.only_missing is None or not args.only_missing else args.only_missing,
5025                        csvSep="," if args.csv_sep is None or not args.csv_sep else args.csv_sep,
5026                        show=True,  # shows all downloaded candles in console
5027                    )
5028
5029                    if args.render_chart is not None and dataReceived is not None:
5030                        iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True
5031
5032                        trader.ShowHistoryChart(
5033                            candles=dataReceived,
5034                            interact=iChart,
5035                            openInBrowser=False,  # False by default, to avoid issues with `permissions denied` to html-file.
5036                        )
5037
5038                else:
5039                    uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]")
5040                    raise Exception("Incorrect value")
5041
5042            elif args.load_history is not None:
5043                histData = trader.LoadHistory(filePath=args.load_history)  # load data from file and show history in console
5044
5045                if args.render_chart is not None and histData is not None:
5046                    iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True
5047                    trader.ticker = os.path.basename(args.load_history)  # use filename as ticker name for PriceGenerator's chart
5048
5049                    trader.ShowHistoryChart(
5050                        candles=histData,
5051                        interact=iChart,
5052                        openInBrowser=False,  # False by default, to avoid issues with `permissions denied` to html-file.
5053                    )
5054
5055            elif args.trade is not None:
5056                if 1 <= len(args.trade) <= 5:
5057                    trader.Trade(
5058                        operation=args.trade[0],
5059                        lots=int(args.trade[1]) if len(args.trade) >= 2 else 1,
5060                        tp=float(args.trade[2]) if len(args.trade) >= 3 else 0.,
5061                        sl=float(args.trade[3]) if len(args.trade) >= 4 else 0.,
5062                        expDate=args.trade[4] if len(args.trade) == 5 else "Undefined",
5063                    )
5064
5065                else:
5066                    uLogger.error("You must specify 1-5 parameters to open trade: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
5067
5068            elif args.buy is not None:
5069                if 0 <= len(args.buy) <= 4:
5070                    trader.Buy(
5071                        lots=int(args.buy[0]) if len(args.buy) >= 1 else 1,
5072                        tp=float(args.buy[1]) if len(args.buy) >= 2 else 0.,
5073                        sl=float(args.buy[2]) if len(args.buy) >= 3 else 0.,
5074                        expDate=args.buy[3] if len(args.buy) == 4 else "Undefined",
5075                    )
5076
5077                else:
5078                    uLogger.error("You must specify 0-4 parameters to open buy position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
5079
5080            elif args.sell is not None:
5081                if 0 <= len(args.sell) <= 4:
5082                    trader.Sell(
5083                        lots=int(args.sell[0]) if len(args.sell) >= 1 else 1,
5084                        tp=float(args.sell[1]) if len(args.sell) >= 2 else 0.,
5085                        sl=float(args.sell[2]) if len(args.sell) >= 3 else 0.,
5086                        expDate=args.sell[3] if len(args.sell) == 4 else "Undefined",
5087                    )
5088
5089                else:
5090                    uLogger.error("You must specify 0-4 parameters to open sell position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
5091
5092            elif args.order:
5093                if 4 <= len(args.order) <= 7:
5094                    trader.Order(
5095                        operation=args.order[0],
5096                        orderType=args.order[1],
5097                        lots=int(args.order[2]),
5098                        targetPrice=float(args.order[3]),
5099                        limitPrice=float(args.order[4]) if len(args.order) >= 5 else 0.,
5100                        stopType=args.order[5] if len(args.order) >= 6 else "Limit",
5101                        expDate=args.order[6] if len(args.order) == 7 else "Undefined",
5102                    )
5103
5104                else:
5105                    uLogger.error("You must specify 4-7 parameters to open order: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]]. See: `python TKSBrokerAPI.py --help`")
5106
5107            elif args.buy_limit:
5108                trader.BuyLimit(lots=int(args.buy_limit[0]), targetPrice=args.buy_limit[1])
5109
5110            elif args.sell_limit:
5111                trader.SellLimit(lots=int(args.sell_limit[0]), targetPrice=args.sell_limit[1])
5112
5113            elif args.buy_stop:
5114                if 2 <= len(args.buy_stop) <= 7:
5115                    trader.BuyStop(
5116                        lots=int(args.buy_stop[0]),
5117                        targetPrice=float(args.buy_stop[1]),
5118                        limitPrice=float(args.buy_stop[2]) if len(args.buy_stop) >= 3 else 0.,
5119                        stopType=args.buy_stop[3] if len(args.buy_stop) >= 4 else "Limit",
5120                        expDate=args.buy_stop[4] if len(args.buy_stop) == 5 else "Undefined",
5121                    )
5122
5123                else:
5124                    uLogger.error("You must specify 2-5 parameters for buy stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
5125
5126            elif args.sell_stop:
5127                if 2 <= len(args.sell_stop) <= 7:
5128                    trader.SellStop(
5129                        lots=int(args.sell_stop[0]),
5130                        targetPrice=float(args.sell_stop[1]),
5131                        limitPrice=float(args.sell_stop[2]) if len(args.sell_stop) >= 3 else 0.,
5132                        stopType=args.sell_stop[3] if len(args.sell_stop) >= 4 else "Limit",
5133                        expDate=args.sell_stop[4] if len(args.sell_stop) == 5 else "Undefined",
5134                    )
5135
5136                else:
5137                    uLogger.error("You must specify 2-5 parameters for sell stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: python TKSBrokerAPI.py --help")
5138
5139            # elif args.buy_order_grid is not None:
5140            #     # update order grid work with api v2
5141            #     if len(args.buy_order_grid) == 2:
5142            #         orderParams = trader.ParseOrderParameters(operation="Buy", **dict(kw.split('=') for kw in args.buy_order_grid))
5143            #
5144            #         for order in orderParams:
5145            #             trader.Order(operation="Buy", lots=order["lot"], price=order["price"])
5146            #
5147            #     else:
5148            #         uLogger.error("To open grid of pending BUY limit-orders (below current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`")
5149            #
5150            # elif args.sell_order_grid is not None:
5151            #     # update order grid work with api v2
5152            #     if len(args.sell_order_grid) >= 2:
5153            #         orderParams = trader.ParseOrderParameters(operation="Sell", **dict(kw.split('=') for kw in args.sell_order_grid))
5154            #
5155            #         for order in orderParams:
5156            #             trader.Order(operation="Sell", lots=order["lot"], price=order["price"])
5157            #
5158            #     else:
5159            #         uLogger.error("To open grid of pending SELL limit-orders (above current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`")
5160
5161            elif args.close_order is not None:
5162                trader.CloseOrders(args.close_order)  # close only one order
5163
5164            elif args.close_orders is not None:
5165                trader.CloseOrders(args.close_orders)  # close list of orders
5166
5167            elif args.close_trade:
5168                if not (args.ticker or args.figi):
5169                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
5170                    raise Exception("Ticker or FIGI required")
5171
5172                if args.ticker:
5173                    trader.CloseTrades([str(args.ticker).upper()])  # close only one trade by ticker (priority)
5174
5175                else:
5176                    trader.CloseTrades([str(args.figi).upper()])  # close only one trade by FIGI
5177
5178            elif args.close_trades is not None:
5179                trader.CloseTrades(args.close_trades)  # close trades for list of tickers
5180
5181            elif args.close_all is not None:
5182                if args.ticker:
5183                    trader.CloseAllByTicker(instrument=str(args.ticker).upper())
5184
5185                elif args.figi:
5186                    trader.CloseAllByFIGI(instrument=str(args.figi).upper())
5187
5188                else:
5189                    trader.CloseAll(*args.close_all)
5190
5191            elif args.limits:
5192                if args.output is not None:
5193                    trader.withdrawalLimitsFile = args.output
5194
5195                trader.OverviewLimits(show=True)
5196
5197            elif args.user_info:
5198                if args.output is not None:
5199                    trader.userInfoFile = args.output
5200
5201                trader.OverviewUserInfo(show=True)
5202
5203            elif args.account:
5204                if args.output is not None:
5205                    trader.userAccountsFile = args.output
5206
5207                trader.OverviewAccounts(show=True)
5208
5209            else:
5210                uLogger.error("There is no command to execute! One of the possible commands must be selected. See help with `--help` key.")
5211                raise Exception("There is no command to execute")
5212
5213    except Exception:
5214        trace = tb.format_exc()
5215        for e in ["socket.gaierror", "nodename nor servname provided", "or not known", "NewConnectionError", "[Errno 8]", "Failed to establish a new connection"]:
5216            if e in trace:
5217                uLogger.error("Check your Internet connection! Failed to establish connection to broker server!")
5218                break
5219
5220        uLogger.debug(trace)
5221        uLogger.debug("Please, check issues or request a new one at https://github.com/Tim55667757/TKSBrokerAPI/issues")
5222        exitCode = 255  # an error occurred, must be open a ticket for this issue
5223
5224    finally:
5225        finish = datetime.now(tzutc())
5226
5227        if exitCode == 0:
5228            if args.more:
5229                uLogger.debug("All operations were finished success (summary code is 0).")
5230
5231        else:
5232            uLogger.error("An issue occurred with TKSBrokerAPI module! See full debug log in [{}] or run TKSBrokerAPI once again with the key `--debug-level 10`. Summary code: {}".format(
5233                os.path.abspath(uLog.defaultLogFile), exitCode,
5234            ))
5235
5236        uLogger.debug(">>> TKSBrokerAPI module work duration: [{}]".format(finish - start))
5237        uLogger.debug(">>> TKSBrokerAPI module finished: [{} UTC], it is [{}] local time".format(
5238            finish.strftime(TKS_PRINT_DATE_TIME_FORMAT),
5239            finish.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
5240        ))
5241        uLogger.debug("=-" * 50)
5242
5243        if not kwargs:
5244            sys.exit(exitCode)
5245
5246        else:
5247            return exitCode

Main function for work with TKSBrokerAPI in the console.

See examples: